@vividcodeai/embeddedcowork-dev 0.0.3-dev-20260507-b76190e8
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 +173 -0
- package/dist/api-types.js +1 -0
- package/dist/auth/auth-store.js +134 -0
- package/dist/auth/http-auth.js +37 -0
- package/dist/auth/manager.js +140 -0
- package/dist/auth/password-hash.js +32 -0
- package/dist/auth/session-manager.js +17 -0
- package/dist/auth/token-manager.js +27 -0
- package/dist/background-processes/manager.js +576 -0
- package/dist/bin.js +24 -0
- package/dist/clients/connection-manager.js +93 -0
- package/dist/config/location.js +57 -0
- package/dist/config/schema.js +71 -0
- package/dist/events/bus.js +45 -0
- package/dist/filesystem/__tests__/search-cache.test.js +40 -0
- package/dist/filesystem/browser.js +292 -0
- package/dist/filesystem/search-cache.js +43 -0
- package/dist/filesystem/search.js +135 -0
- package/dist/index.js +466 -0
- package/dist/launcher.js +149 -0
- package/dist/loader.js +21 -0
- package/dist/logger.js +109 -0
- package/dist/opencode-config/README.md +32 -0
- package/dist/opencode-config/opencode.jsonc +3 -0
- package/dist/opencode-config/package.json +9 -0
- package/dist/opencode-config/plugin/embeddedcowork.ts +62 -0
- package/dist/opencode-config/plugin/lib/background-process.ts +265 -0
- package/dist/opencode-config/plugin/lib/client.ts +133 -0
- package/dist/opencode-config/plugin/lib/request.ts +214 -0
- package/dist/opencode-config.js +15 -0
- package/dist/plugins/channel.js +40 -0
- package/dist/plugins/handlers.js +17 -0
- package/dist/plugins/voice-mode.js +78 -0
- package/dist/releases/dev-release-monitor.js +75 -0
- package/dist/releases/release-monitor.js +107 -0
- package/dist/runtime-paths.js +67 -0
- package/dist/server/__tests__/network-addresses.test.js +68 -0
- package/dist/server/__tests__/remote-proxy.test.js +204 -0
- package/dist/server/http-server.js +996 -0
- package/dist/server/network-addresses.js +114 -0
- package/dist/server/remote-proxy.js +466 -0
- package/dist/server/routes/auth-pages/login.html +135 -0
- package/dist/server/routes/auth-pages/token.html +93 -0
- package/dist/server/routes/auth.js +149 -0
- package/dist/server/routes/background-processes.js +78 -0
- package/dist/server/routes/events.js +66 -0
- package/dist/server/routes/filesystem.js +43 -0
- package/dist/server/routes/meta.js +44 -0
- package/dist/server/routes/plugin.js +70 -0
- package/dist/server/routes/remote-proxy.js +42 -0
- package/dist/server/routes/remote-servers.js +142 -0
- package/dist/server/routes/settings.js +69 -0
- package/dist/server/routes/sidecars.js +46 -0
- package/dist/server/routes/speech.js +63 -0
- package/dist/server/routes/storage.js +52 -0
- package/dist/server/routes/workspaces.js +221 -0
- package/dist/server/routes/worktrees.js +156 -0
- package/dist/server/tls.js +224 -0
- package/dist/settings/binaries.js +37 -0
- package/dist/settings/merge-patch.js +33 -0
- package/dist/settings/migrate.js +238 -0
- package/dist/settings/public-config.js +33 -0
- package/dist/settings/service.js +101 -0
- package/dist/settings/yaml-doc-store.js +96 -0
- package/dist/sidecars/manager.js +193 -0
- package/dist/speech/providers/openai-compatible.js +189 -0
- package/dist/speech/service.js +58 -0
- package/dist/storage/instance-store.js +56 -0
- package/dist/ui/__tests__/remote-ui.test.js +67 -0
- package/dist/ui/remote-ui.js +462 -0
- package/dist/workspaces/__tests__/spawn.test.js +139 -0
- package/dist/workspaces/git-mutations.js +98 -0
- package/dist/workspaces/git-status.js +323 -0
- package/dist/workspaces/git-worktrees.js +216 -0
- package/dist/workspaces/instance-events.js +180 -0
- package/dist/workspaces/manager.js +420 -0
- package/dist/workspaces/opencode-auth.js +16 -0
- package/dist/workspaces/runtime.js +366 -0
- package/dist/workspaces/spawn.js +219 -0
- package/dist/workspaces/worktree-directory.js +74 -0
- package/dist/workspaces/worktree-map.js +116 -0
- package/package.json +57 -0
- package/public/apple-touch-icon-180x180.png +0 -0
- package/public/assets/ChangesTab-C2DJXDf9.js +2 -0
- package/public/assets/DiffToolbar-De-3SRCF.js +1 -0
- package/public/assets/EmbeddedCowork-Icon-DSw5nKk7.png +0 -0
- package/public/assets/FilesTab-BuQ00MEc.js +2 -0
- package/public/assets/GitChangesTab-D9bf2jkM.js +2 -0
- package/public/assets/SplitFilePanel-B-3h60o2.js +1 -0
- package/public/assets/StatusTab-D5s19fRN.js +1 -0
- package/public/assets/abap-BdImnpbu.js +1 -0
- package/public/assets/actionscript-3-CfeIJUat.js +1 -0
- package/public/assets/ada-bCR0ucgS.js +1 -0
- package/public/assets/andromeeda-C-Jbm3Hp.js +1 -0
- package/public/assets/angular-html-CU67Zn6k.js +1 -0
- package/public/assets/angular-ts-BwZT4LLn.js +1 -0
- package/public/assets/apache-Pmp26Uib.js +1 -0
- package/public/assets/apex-DhZLUxFE.js +1 -0
- package/public/assets/apl-dKokRX4l.js +1 -0
- package/public/assets/applescript-Co6uUVPk.js +1 -0
- package/public/assets/ara-BRHolxvo.js +1 -0
- package/public/assets/asciidoc-Dv7Oe6Be.js +1 -0
- package/public/assets/asm-D_Q5rh1f.js +1 -0
- package/public/assets/astro-CbQHKStN.js +1 -0
- package/public/assets/aurora-x-D-2ljcwZ.js +1 -0
- package/public/assets/awk-DMzUqQB5.js +1 -0
- package/public/assets/ayu-dark-Cv9koXgw.js +1 -0
- package/public/assets/ballerina-BFfxhgS-.js +1 -0
- package/public/assets/bat-BkioyH1T.js +1 -0
- package/public/assets/beancount-k_qm7-4y.js +1 -0
- package/public/assets/berry-D08WgyRC.js +1 -0
- package/public/assets/bibtex-CHM0blh-.js +1 -0
- package/public/assets/bicep-Bmn6On1c.js +1 -0
- package/public/assets/blade-DVc8C-J4.js +1 -0
- package/public/assets/bsl-BO_Y6i37.js +1 -0
- package/public/assets/bundle-full-CdNbUxmo.js +13 -0
- package/public/assets/c-BIGW1oBm.js +1 -0
- package/public/assets/cadence-Bv_4Rxtq.js +1 -0
- package/public/assets/cairo-KRGpt6FW.js +1 -0
- package/public/assets/catppuccin-frappe-DFWUc33u.js +1 -0
- package/public/assets/catppuccin-latte-C9dUb6Cb.js +1 -0
- package/public/assets/catppuccin-macchiato-DQyhUUbL.js +1 -0
- package/public/assets/catppuccin-mocha-D87Tk5Gz.js +1 -0
- package/public/assets/clarity-D53aC0YG.js +1 -0
- package/public/assets/clojure-P80f7IUj.js +1 -0
- package/public/assets/cmake-D1j8_8rp.js +1 -0
- package/public/assets/cobol-nwyudZeR.js +1 -0
- package/public/assets/codeowners-Bp6g37R7.js +1 -0
- package/public/assets/codeql-DsOJ9woJ.js +1 -0
- package/public/assets/coffee-Ch7k5sss.js +1 -0
- package/public/assets/common-lisp-Cg-RD9OK.js +1 -0
- package/public/assets/coq-DkFqJrB1.js +1 -0
- package/public/assets/core-DhEqZVGG.js +1 -0
- package/public/assets/cpp-CofmeUqb.js +1 -0
- package/public/assets/crystal-tKQVLTB8.js +1 -0
- package/public/assets/csharp-CX12Zw3r.js +1 -0
- package/public/assets/css-DPfMkruS.js +1 -0
- package/public/assets/csv-fuZLfV_i.js +1 -0
- package/public/assets/cue-D82EKSYY.js +1 -0
- package/public/assets/cypher-COkxafJQ.js +1 -0
- package/public/assets/d-85-TOEBH.js +1 -0
- package/public/assets/dark-plus-eOWES_5F.js +1 -0
- package/public/assets/dart-CF10PKvl.js +1 -0
- package/public/assets/dax-CEL-wOlO.js +1 -0
- package/public/assets/desktop-BmXAJ9_W.js +1 -0
- package/public/assets/diff-D97Zzqfu.js +1 -0
- package/public/assets/diff-viewer-B1l_VZc1.js +1 -0
- package/public/assets/docker-BcOcwvcX.js +1 -0
- package/public/assets/dotenv-Da5cRb03.js +1 -0
- package/public/assets/dracula-BzJJZx-M.js +1 -0
- package/public/assets/dracula-soft-BXkSAIEj.js +1 -0
- package/public/assets/dream-maker-BtqSS_iP.js +1 -0
- package/public/assets/edge-BkV0erSs.js +1 -0
- package/public/assets/elixir-CDX3lj18.js +1 -0
- package/public/assets/elm-DbKCFpqz.js +1 -0
- package/public/assets/emacs-lisp-C9XAeP06.js +1 -0
- package/public/assets/erb-BOJIQeun.js +1 -0
- package/public/assets/erlang-DsQrWhSR.js +1 -0
- package/public/assets/event-DjZVAIBO.js +1 -0
- package/public/assets/everforest-dark-BgDCqdQA.js +1 -0
- package/public/assets/everforest-light-C8M2exoo.js +1 -0
- package/public/assets/fast-diff-vendor-DgdwVvTQ.js +1 -0
- package/public/assets/fennel-BYunw83y.js +1 -0
- package/public/assets/fish-BvzEVeQv.js +1 -0
- package/public/assets/fluent-C4IJs8-o.js +1 -0
- package/public/assets/fortran-fixed-form-BZjJHVRy.js +1 -0
- package/public/assets/fortran-free-form-D22FLkUw.js +1 -0
- package/public/assets/fsharp-CXgrBDvD.js +1 -0
- package/public/assets/gdresource-B7Tvp0Sc.js +1 -0
- package/public/assets/gdscript-DTMYz4Jt.js +1 -0
- package/public/assets/gdshader-DkwncUOv.js +1 -0
- package/public/assets/genie-D0YGMca9.js +1 -0
- package/public/assets/gherkin-DyxjwDmM.js +1 -0
- package/public/assets/git-commit-F4YmCXRG.js +1 -0
- package/public/assets/git-diff-vendor-CSgooKT_.js +52 -0
- package/public/assets/git-diff-vendor-HAZkIolJ.css +19 -0
- package/public/assets/git-rebase-r7XF79zn.js +1 -0
- package/public/assets/github-dark-DHJKELXO.js +1 -0
- package/public/assets/github-dark-default-Cuk6v7N8.js +1 -0
- package/public/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
- package/public/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
- package/public/assets/github-light-DAi9KRSo.js +1 -0
- package/public/assets/github-light-default-D7oLnXFd.js +1 -0
- package/public/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
- package/public/assets/gleam-BspZqrRM.js +1 -0
- package/public/assets/glimmer-js-Rg0-pVw9.js +1 -0
- package/public/assets/glimmer-ts-U6CK756n.js +1 -0
- package/public/assets/glsl-DplSGwfg.js +1 -0
- package/public/assets/gnuplot-DdkO51Og.js +1 -0
- package/public/assets/go-Dn2_MT6a.js +1 -0
- package/public/assets/graphql-ChdNCCLP.js +1 -0
- package/public/assets/groovy-gcz8RCvz.js +1 -0
- package/public/assets/gruvbox-dark-hard-CFHQjOhq.js +1 -0
- package/public/assets/gruvbox-dark-medium-GsRaNv29.js +1 -0
- package/public/assets/gruvbox-dark-soft-CVdnzihN.js +1 -0
- package/public/assets/gruvbox-light-hard-CH1njM8p.js +1 -0
- package/public/assets/gruvbox-light-medium-DRw_LuNl.js +1 -0
- package/public/assets/gruvbox-light-soft-hJgmCMqR.js +1 -0
- package/public/assets/hack-CaT9iCJl.js +1 -0
- package/public/assets/haml-B8DHNrY2.js +1 -0
- package/public/assets/handlebars-BL8al0AC.js +1 -0
- package/public/assets/haskell-Df6bDoY_.js +1 -0
- package/public/assets/haxe-CzTSHFRz.js +1 -0
- package/public/assets/hcl-BWvSN4gD.js +1 -0
- package/public/assets/highlight-vendor-8FKMu9os.js +10 -0
- package/public/assets/hjson-D5-asLiD.js +1 -0
- package/public/assets/hlsl-D3lLCCz7.js +1 -0
- package/public/assets/houston-DnULxvSX.js +1 -0
- package/public/assets/html-GMplVEZG.js +1 -0
- package/public/assets/html-derivative-BFtXZ54Q.js +1 -0
- package/public/assets/http-jrhK8wxY.js +1 -0
- package/public/assets/hurl-irOxFIW8.js +1 -0
- package/public/assets/hxml-Bvhsp5Yf.js +1 -0
- package/public/assets/hy-DFXneXwc.js +1 -0
- package/public/assets/imba-DGztddWO.js +1 -0
- package/public/assets/index-B2LsA7hD.js +1 -0
- package/public/assets/index-BErmCqgL.js +1 -0
- package/public/assets/index-BKMyzTSR.js +1 -0
- package/public/assets/index-BKvZBimW.js +1 -0
- package/public/assets/index-BQeBs108.js +1 -0
- package/public/assets/index-BqQARTCd.js +1 -0
- package/public/assets/index-C9Tl2tHH.js +2 -0
- package/public/assets/index-CLSJ4cO9.js +1 -0
- package/public/assets/index-DElsPAzQ.css +1 -0
- package/public/assets/index-ixx_g9gD.js +1 -0
- package/public/assets/ini-BEwlwnbL.js +1 -0
- package/public/assets/java-CylS5w8V.js +1 -0
- package/public/assets/javascript-wDzz0qaB.js +1 -0
- package/public/assets/jinja-4LBKfQ-Z.js +1 -0
- package/public/assets/jison-wvAkD_A8.js +1 -0
- package/public/assets/json-Cp-IABpG.js +1 -0
- package/public/assets/json5-C9tS-k6U.js +1 -0
- package/public/assets/jsonc-Des-eS-w.js +1 -0
- package/public/assets/jsonl-DcaNXYhu.js +1 -0
- package/public/assets/jsonnet-DFQXde-d.js +1 -0
- package/public/assets/jssm-C2t-YnRu.js +1 -0
- package/public/assets/jsx-g9-lgVsj.js +1 -0
- package/public/assets/julia-C8NyazO9.js +1 -0
- package/public/assets/kanagawa-dragon-CkXjmgJE.js +1 -0
- package/public/assets/kanagawa-lotus-CfQXZHmo.js +1 -0
- package/public/assets/kanagawa-wave-DWedfzmr.js +1 -0
- package/public/assets/kdl-DV7GczEv.js +1 -0
- package/public/assets/kotlin-BdnUsdx6.js +1 -0
- package/public/assets/kusto-BvAqAH-y.js +1 -0
- package/public/assets/laserwave-DUszq2jm.js +1 -0
- package/public/assets/latex-BUKiar2Z.js +1 -0
- package/public/assets/lean-DP1Csr6i.js +1 -0
- package/public/assets/less-B1dDrJ26.js +1 -0
- package/public/assets/light-plus-B7mTdjB0.js +1 -0
- package/public/assets/liquid-DYVedYrR.js +1 -0
- package/public/assets/llvm-BtvRca6l.js +1 -0
- package/public/assets/loading-CQjaT4lJ.js +2 -0
- package/public/assets/loading-CugGjKDZ.css +1 -0
- package/public/assets/log-2UxHyX5q.js +1 -0
- package/public/assets/logo-BtOb2qkB.js +1 -0
- package/public/assets/lua-BbnMAYS6.js +1 -0
- package/public/assets/luau-CXu1NL6O.js +1 -0
- package/public/assets/main-C1yBw4P8.js +53 -0
- package/public/assets/make-CHLpvVh8.js +1 -0
- package/public/assets/markdown-Cvjx9yec.js +1 -0
- package/public/assets/markdown-D5eIdNMf.js +58 -0
- package/public/assets/marko-CPi9NSCl.js +1 -0
- package/public/assets/material-theme-D5KoaKCx.js +1 -0
- package/public/assets/material-theme-darker-BfHTSMKl.js +1 -0
- package/public/assets/material-theme-lighter-B0m2ddpp.js +1 -0
- package/public/assets/material-theme-ocean-CyktbL80.js +1 -0
- package/public/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
- package/public/assets/matlab-D7o27uSR.js +1 -0
- package/public/assets/mdc-DUICxH0z.js +1 -0
- package/public/assets/mdx-Cmh6b_Ma.js +1 -0
- package/public/assets/mermaid-DKYwYmdq.js +1 -0
- package/public/assets/min-dark-CafNBF8u.js +1 -0
- package/public/assets/min-light-CTRr51gU.js +1 -0
- package/public/assets/mipsasm-CKIfxQSi.js +1 -0
- package/public/assets/mojo-1DNp92w6.js +1 -0
- package/public/assets/monaco-viewer-9Byc1Kpy.js +26 -0
- package/public/assets/monokai-D4h5O-jR.js +1 -0
- package/public/assets/move-Bu9oaDYs.js +1 -0
- package/public/assets/narrat-DRg8JJMk.js +1 -0
- package/public/assets/nextflow-CUEJCptM.js +1 -0
- package/public/assets/nginx-DknmC5AR.js +1 -0
- package/public/assets/night-owl-C39BiMTA.js +1 -0
- package/public/assets/nim-CVrawwO9.js +1 -0
- package/public/assets/nix-BbRYJGeE.js +1 -0
- package/public/assets/nord-Ddv68eIx.js +1 -0
- package/public/assets/nushell-C-sUppwS.js +1 -0
- package/public/assets/objective-c-DXmwc3jG.js +1 -0
- package/public/assets/objective-cpp-CLxacb5B.js +1 -0
- package/public/assets/ocaml-C0hk2d4L.js +1 -0
- package/public/assets/one-dark-pro-DVMEJ2y_.js +1 -0
- package/public/assets/one-light-PoHY5YXO.js +1 -0
- package/public/assets/pascal-D93ZcfNL.js +1 -0
- package/public/assets/perl-C0TMdlhV.js +1 -0
- package/public/assets/php-CDn_0X-4.js +1 -0
- package/public/assets/pkl-u5AG7uiY.js +1 -0
- package/public/assets/plastic-3e1v2bzS.js +1 -0
- package/public/assets/plsql-ChMvpjG-.js +1 -0
- package/public/assets/po-BTJTHyun.js +1 -0
- package/public/assets/poimandres-CS3Unz2-.js +1 -0
- package/public/assets/polar-C0HS_06l.js +1 -0
- package/public/assets/postcss-CXtECtnM.js +1 -0
- package/public/assets/powerquery-CEu0bR-o.js +1 -0
- package/public/assets/powershell-Dpen1YoG.js +1 -0
- package/public/assets/prisma-Dd19v3D-.js +1 -0
- package/public/assets/prolog-CbFg5uaA.js +1 -0
- package/public/assets/proto-DyJlTyXw.js +1 -0
- package/public/assets/pug-CGlum2m_.js +1 -0
- package/public/assets/puppet-BMWR74SV.js +1 -0
- package/public/assets/purescript-CklMAg4u.js +1 -0
- package/public/assets/python-B6aJPvgy.js +1 -0
- package/public/assets/qml-3beO22l8.js +1 -0
- package/public/assets/qmldir-C8lEn-DE.js +1 -0
- package/public/assets/qss-IeuSbFQv.js +1 -0
- package/public/assets/r-DiinP2Uv.js +1 -0
- package/public/assets/racket-BqYA7rlc.js +1 -0
- package/public/assets/raku-DXvB9xmW.js +1 -0
- package/public/assets/razor-WgofotgN.js +1 -0
- package/public/assets/red-bN70gL4F.js +1 -0
- package/public/assets/reg-C-SQnVFl.js +1 -0
- package/public/assets/regexp-CDVJQ6XC.js +1 -0
- package/public/assets/rel-C3B-1QV4.js +1 -0
- package/public/assets/riscv-BM1_JUlF.js +1 -0
- package/public/assets/rose-pine-BHrmToEH.js +1 -0
- package/public/assets/rose-pine-dawn-CnK8MTSM.js +1 -0
- package/public/assets/rose-pine-moon-NleAzG8P.js +1 -0
- package/public/assets/rosmsg-BJDFO7_C.js +1 -0
- package/public/assets/rst-B0xPkSld.js +1 -0
- package/public/assets/ruby-BvKwtOVI.js +1 -0
- package/public/assets/rust-B1yitclQ.js +1 -0
- package/public/assets/sas-cz2c8ADy.js +1 -0
- package/public/assets/sass-Cj5Yp3dK.js +1 -0
- package/public/assets/scala-C151Ov-r.js +1 -0
- package/public/assets/scheme-C98Dy4si.js +1 -0
- package/public/assets/scss-OYdSNvt2.js +1 -0
- package/public/assets/sdbl-DVxCFoDh.js +1 -0
- package/public/assets/shaderlab-Dg9Lc6iA.js +1 -0
- package/public/assets/shellscript-Yzrsuije.js +1 -0
- package/public/assets/shellsession-BADoaaVG.js +1 -0
- package/public/assets/slack-dark-BthQWCQV.js +1 -0
- package/public/assets/slack-ochin-DqwNpetd.js +1 -0
- package/public/assets/smalltalk-BERRCDM3.js +1 -0
- package/public/assets/snazzy-light-Bw305WKR.js +1 -0
- package/public/assets/solarized-dark-DXbdFlpD.js +1 -0
- package/public/assets/solarized-light-L9t79GZl.js +1 -0
- package/public/assets/solidity-BbcW6ACK.js +1 -0
- package/public/assets/soy-Brmx7dQM.js +1 -0
- package/public/assets/sparql-rVzFXLq3.js +1 -0
- package/public/assets/splunk-BtCnVYZw.js +1 -0
- package/public/assets/sql-BLtJtn59.js +1 -0
- package/public/assets/ssh-config-_ykCGR6B.js +1 -0
- package/public/assets/stata-BH5u7GGu.js +1 -0
- package/public/assets/stylus-BEDo0Tqx.js +1 -0
- package/public/assets/svelte-3Dk4HxPD.js +1 -0
- package/public/assets/swift-Dg5xB15N.js +1 -0
- package/public/assets/synthwave-84-CbfX1IO0.js +1 -0
- package/public/assets/system-verilog-CnnmHF94.js +1 -0
- package/public/assets/systemd-4A_iFExJ.js +1 -0
- package/public/assets/talonscript-CkByrt1z.js +1 -0
- package/public/assets/tasl-QIJgUcNo.js +1 -0
- package/public/assets/tcl-dwOrl1Do.js +1 -0
- package/public/assets/templ-W15q3VgB.js +1 -0
- package/public/assets/terraform-BETggiCN.js +1 -0
- package/public/assets/tex-Cppo0RY3.js +1 -0
- package/public/assets/todo-uxdyLWei.js +1 -0
- package/public/assets/tokyo-night-hegEt444.js +1 -0
- package/public/assets/toml-vGWfd6FD.js +1 -0
- package/public/assets/tool-call-C_JEoVSV.js +60 -0
- package/public/assets/ts-tags-zn1MmPIZ.js +1 -0
- package/public/assets/tsv-B_m7g4N7.js +1 -0
- package/public/assets/tsx-COt5Ahok.js +1 -0
- package/public/assets/turtle-BsS91CYL.js +1 -0
- package/public/assets/twig-CO9l9SDP.js +1 -0
- package/public/assets/typescript-BPQ3VLAy.js +1 -0
- package/public/assets/typespec-Df68jz8_.js +1 -0
- package/public/assets/typst-DHCkPAjA.js +1 -0
- package/public/assets/unified-picker-ePaJEYDm.js +1 -0
- package/public/assets/v-BcVCzyr7.js +1 -0
- package/public/assets/vala-CsfeWuGM.js +1 -0
- package/public/assets/vb-D17OF-Vu.js +1 -0
- package/public/assets/verilog-BQ8w6xss.js +1 -0
- package/public/assets/vesper-DU1UobuO.js +1 -0
- package/public/assets/vhdl-CeAyd5Ju.js +1 -0
- package/public/assets/viml-CJc9bBzg.js +1 -0
- package/public/assets/vitesse-black-Bkuqu6BP.js +1 -0
- package/public/assets/vitesse-dark-D0r3Knsf.js +1 -0
- package/public/assets/vitesse-light-CVO1_9PV.js +1 -0
- package/public/assets/vue-CCoi5OLL.js +1 -0
- package/public/assets/vue-html-DAAvJJDi.js +1 -0
- package/public/assets/vue-vine-_Ih-lPRR.js +1 -0
- package/public/assets/vyper-CDx5xZoG.js +1 -0
- package/public/assets/wasm-CG6Dc4jp.js +1 -0
- package/public/assets/wasm-MzD3tlZU.js +1 -0
- package/public/assets/wenyan-BV7otONQ.js +1 -0
- package/public/assets/wgsl-Dx-B1_4e.js +1 -0
- package/public/assets/wikitext-BhOHFoWU.js +1 -0
- package/public/assets/wit-5i3qLPDT.js +1 -0
- package/public/assets/wolfram-lXgVvXCa.js +1 -0
- package/public/assets/wrap-text-S8HH4qqP.js +1 -0
- package/public/assets/xml-sdJ4AIDG.js +1 -0
- package/public/assets/xsl-CtQFsRM5.js +1 -0
- package/public/assets/yaml-Buea-lGh.js +1 -0
- package/public/assets/zenscript-DVFEvuxE.js +1 -0
- package/public/assets/zig-VOosw3JB.js +1 -0
- package/public/favicon.ico +0 -0
- package/public/index.html +44 -0
- package/public/loading.html +33 -0
- package/public/logo.png +0 -0
- package/public/manifest.webmanifest +1 -0
- package/public/maskable-icon-512x512.png +0 -0
- package/public/monaco/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
- package/public/monaco/vs/base/worker/workerMain.js +31 -0
- package/public/monaco/vs/basic-languages/cpp/cpp.js +10 -0
- package/public/monaco/vs/basic-languages/kotlin/kotlin.js +10 -0
- package/public/monaco/vs/basic-languages/markdown/markdown.js +10 -0
- package/public/monaco/vs/basic-languages/python/python.js +10 -0
- package/public/monaco/vs/editor/editor.main.css +8 -0
- package/public/monaco/vs/editor/editor.main.js +798 -0
- package/public/monaco/vs/language/css/cssMode.js +13 -0
- package/public/monaco/vs/language/css/cssWorker.js +77 -0
- package/public/monaco/vs/language/html/htmlMode.js +13 -0
- package/public/monaco/vs/language/html/htmlWorker.js +454 -0
- package/public/monaco/vs/language/json/jsonMode.js +19 -0
- package/public/monaco/vs/language/json/jsonWorker.js +42 -0
- package/public/monaco/vs/language/typescript/tsMode.js +20 -0
- package/public/monaco/vs/language/typescript/tsWorker.js +51328 -0
- package/public/monaco/vs/loader.js +11 -0
- package/public/monaco.worker.js +7 -0
- package/public/pwa-192x192.png +0 -0
- package/public/pwa-512x512.png +0 -0
- package/public/pwa-64x64.png +0 -0
- package/public/registerSW.js +1 -0
- package/public/sw.js +1 -0
- package/public/ui-version.json +3 -0
- package/public/workbox-60d14903.js +1 -0
|
@@ -0,0 +1,996 @@
|
|
|
1
|
+
import Fastify from "fastify";
|
|
2
|
+
import cors from "@fastify/cors";
|
|
3
|
+
import fastifyStatic from "@fastify/static";
|
|
4
|
+
import replyFrom from "@fastify/reply-from";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import { connect as connectTcp } from "net";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import { Readable } from "stream";
|
|
9
|
+
import { pipeline } from "stream/promises";
|
|
10
|
+
import { connect as connectTls } from "tls";
|
|
11
|
+
import { fetch } from "undici";
|
|
12
|
+
import { isValidWorktreeSlug } from "../workspaces/git-worktrees";
|
|
13
|
+
import { resolveWorktreeDirectory } from "../workspaces/worktree-directory";
|
|
14
|
+
import { registerWorkspaceRoutes } from "./routes/workspaces";
|
|
15
|
+
import { registerSettingsRoutes } from "./routes/settings";
|
|
16
|
+
import { registerFilesystemRoutes } from "./routes/filesystem";
|
|
17
|
+
import { registerMetaRoutes } from "./routes/meta";
|
|
18
|
+
import { registerEventRoutes } from "./routes/events";
|
|
19
|
+
import { registerStorageRoutes } from "./routes/storage";
|
|
20
|
+
import { registerPluginRoutes } from "./routes/plugin";
|
|
21
|
+
import { registerBackgroundProcessRoutes } from "./routes/background-processes";
|
|
22
|
+
import { registerWorktreeRoutes } from "./routes/worktrees";
|
|
23
|
+
import { registerSpeechRoutes } from "./routes/speech";
|
|
24
|
+
import { registerRemoteServerRoutes } from "./routes/remote-servers";
|
|
25
|
+
import { registerRemoteProxyRoutes } from "./routes/remote-proxy";
|
|
26
|
+
import { registerSideCarRoutes } from "./routes/sidecars";
|
|
27
|
+
import { BackgroundProcessManager } from "../background-processes/manager";
|
|
28
|
+
import { registerAuthRoutes } from "./routes/auth";
|
|
29
|
+
import { sendUnauthorized, wantsHtml } from "../auth/http-auth";
|
|
30
|
+
export function createHttpServer(deps) {
|
|
31
|
+
// Fastify's type-level RawServer inference gets noisy when toggling HTTP vs HTTPS.
|
|
32
|
+
// We keep the runtime behavior correct and cast the instance to a generic FastifyInstance.
|
|
33
|
+
const app = Fastify({
|
|
34
|
+
logger: false,
|
|
35
|
+
...(deps.protocol === "https" && deps.httpsOptions ? { https: deps.httpsOptions } : {}),
|
|
36
|
+
});
|
|
37
|
+
const proxyLogger = deps.logger.child({ component: "proxy" });
|
|
38
|
+
const apiLogger = deps.logger.child({ component: "http" });
|
|
39
|
+
const sseLogger = deps.logger.child({ component: "sse" });
|
|
40
|
+
const sseClients = new Set();
|
|
41
|
+
const registerSseClient = (cleanup) => {
|
|
42
|
+
sseClients.add(cleanup);
|
|
43
|
+
return () => sseClients.delete(cleanup);
|
|
44
|
+
};
|
|
45
|
+
const closeSseClients = () => {
|
|
46
|
+
for (const cleanup of Array.from(sseClients)) {
|
|
47
|
+
cleanup();
|
|
48
|
+
}
|
|
49
|
+
sseClients.clear();
|
|
50
|
+
};
|
|
51
|
+
app.addHook("onRequest", (request, _reply, done) => {
|
|
52
|
+
;
|
|
53
|
+
request.__logMeta = {
|
|
54
|
+
start: process.hrtime.bigint(),
|
|
55
|
+
};
|
|
56
|
+
done();
|
|
57
|
+
});
|
|
58
|
+
app.addHook("onResponse", (request, reply, done) => {
|
|
59
|
+
const meta = request.__logMeta;
|
|
60
|
+
const durationMs = meta ? Number((process.hrtime.bigint() - meta.start) / BigInt(1000000)) : undefined;
|
|
61
|
+
const base = {
|
|
62
|
+
method: request.method,
|
|
63
|
+
url: request.url,
|
|
64
|
+
status: reply.statusCode,
|
|
65
|
+
durationMs,
|
|
66
|
+
};
|
|
67
|
+
apiLogger.debug(base, "HTTP request completed");
|
|
68
|
+
if (apiLogger.isLevelEnabled("trace")) {
|
|
69
|
+
apiLogger.trace({ ...base, params: request.params, query: request.query, body: request.body }, "HTTP request payload");
|
|
70
|
+
}
|
|
71
|
+
done();
|
|
72
|
+
});
|
|
73
|
+
const allowedDevOrigins = new Set(["http://localhost:3000", "http://127.0.0.1:3000"]);
|
|
74
|
+
const isLoopbackHost = (host) => host === "127.0.0.1" || host === "::1" || host.startsWith("127.");
|
|
75
|
+
const getSelfOrigins = () => {
|
|
76
|
+
const origins = new Set();
|
|
77
|
+
const candidates = [deps.serverMeta.localUrl, deps.serverMeta.remoteUrl];
|
|
78
|
+
for (const candidate of candidates) {
|
|
79
|
+
if (!candidate)
|
|
80
|
+
continue;
|
|
81
|
+
try {
|
|
82
|
+
origins.add(new URL(candidate).origin);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// ignore
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for (const addr of deps.serverMeta.addresses ?? []) {
|
|
89
|
+
try {
|
|
90
|
+
origins.add(new URL(addr.remoteUrl).origin);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// ignore
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return origins;
|
|
97
|
+
};
|
|
98
|
+
app.register(cors, {
|
|
99
|
+
origin: (origin, cb) => {
|
|
100
|
+
if (!origin) {
|
|
101
|
+
cb(null, true);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const selfOrigins = getSelfOrigins();
|
|
105
|
+
if (selfOrigins.has(origin)) {
|
|
106
|
+
cb(null, true);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (allowedDevOrigins.has(origin)) {
|
|
110
|
+
cb(null, true);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// When we bind to a non-loopback host (e.g., 0.0.0.0 or LAN IP), allow cross-origin UI access.
|
|
114
|
+
if (deps.bindHost === "0.0.0.0" || !isLoopbackHost(deps.bindHost)) {
|
|
115
|
+
cb(null, true);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
cb(null, false);
|
|
119
|
+
},
|
|
120
|
+
credentials: true,
|
|
121
|
+
});
|
|
122
|
+
app.register(replyFrom, {
|
|
123
|
+
contentTypesToEncode: [],
|
|
124
|
+
undici: {
|
|
125
|
+
connections: 16,
|
|
126
|
+
pipelining: 1,
|
|
127
|
+
bodyTimeout: 0,
|
|
128
|
+
headersTimeout: 0,
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
const backgroundProcessManager = new BackgroundProcessManager({
|
|
132
|
+
workspaceManager: deps.workspaceManager,
|
|
133
|
+
eventBus: deps.eventBus,
|
|
134
|
+
logger: deps.logger.child({ component: "background-processes" }),
|
|
135
|
+
});
|
|
136
|
+
registerAuthRoutes(app, { authManager: deps.authManager });
|
|
137
|
+
app.addHook("preHandler", (request, reply, done) => {
|
|
138
|
+
const rawUrl = request.raw.url ?? request.url;
|
|
139
|
+
const pathname = (rawUrl.split("?")[0] ?? "").trim();
|
|
140
|
+
const publicApiPaths = new Set(["/api/auth/login", "/api/auth/token", "/api/auth/status", "/api/auth/logout"]);
|
|
141
|
+
const publicPagePaths = new Set(["/login"]);
|
|
142
|
+
if (deps.authManager.isTokenBootstrapEnabled()) {
|
|
143
|
+
publicPagePaths.add("/auth/token");
|
|
144
|
+
}
|
|
145
|
+
const isLoopbackRemoteProxyDelete = request.method === "DELETE" &&
|
|
146
|
+
pathname.startsWith("/api/remote-proxy/sessions/") &&
|
|
147
|
+
deps.authManager.isLoopbackRequest(request);
|
|
148
|
+
if (publicApiPaths.has(pathname) || publicPagePaths.has(pathname) || isLoopbackRemoteProxyDelete) {
|
|
149
|
+
done();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const session = deps.authManager.getSessionFromRequest(request);
|
|
153
|
+
const requiresAuthForApi = pathname.startsWith("/api/") || pathname.startsWith("/workspaces/") || pathname.startsWith("/sidecars/");
|
|
154
|
+
if (requiresAuthForApi && !session) {
|
|
155
|
+
// Allow OpenCode plugin -> EmbeddedCowork calls with per-instance basic auth.
|
|
156
|
+
const pluginMatch = pathname.match(/^\/workspaces\/([^/]+)\/plugin(?:\/|$)/);
|
|
157
|
+
if (pluginMatch) {
|
|
158
|
+
const workspaceId = pluginMatch[1];
|
|
159
|
+
const expected = deps.workspaceManager.getInstanceAuthorizationHeader(workspaceId);
|
|
160
|
+
const provided = Array.isArray(request.headers.authorization)
|
|
161
|
+
? request.headers.authorization[0]
|
|
162
|
+
: request.headers.authorization;
|
|
163
|
+
if (expected && provided && provided === expected) {
|
|
164
|
+
done();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
sendUnauthorized(request, reply);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (!session && wantsHtml(request)) {
|
|
172
|
+
reply.redirect("/login");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
done();
|
|
176
|
+
});
|
|
177
|
+
app.get("/", async (request, reply) => {
|
|
178
|
+
const session = deps.authManager.getSessionFromRequest(request);
|
|
179
|
+
if (!session) {
|
|
180
|
+
reply.redirect("/login");
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (deps.uiDevServerUrl) {
|
|
184
|
+
await proxyToDevServer(request, reply, deps.uiDevServerUrl);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const uiDir = deps.uiStaticDir;
|
|
188
|
+
const indexPath = path.join(uiDir, "index.html");
|
|
189
|
+
if (uiDir && fs.existsSync(indexPath)) {
|
|
190
|
+
reply.type("text/html").send(fs.readFileSync(indexPath, "utf-8"));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
reply.code(404).send({ message: "UI bundle missing" });
|
|
194
|
+
});
|
|
195
|
+
registerWorkspaceRoutes(app, { workspaceManager: deps.workspaceManager });
|
|
196
|
+
registerSettingsRoutes(app, { settings: deps.settings, logger: apiLogger });
|
|
197
|
+
registerFilesystemRoutes(app, { fileSystemBrowser: deps.fileSystemBrowser });
|
|
198
|
+
registerMetaRoutes(app, { serverMeta: deps.serverMeta });
|
|
199
|
+
registerEventRoutes(app, {
|
|
200
|
+
eventBus: deps.eventBus,
|
|
201
|
+
registerClient: registerSseClient,
|
|
202
|
+
logger: sseLogger,
|
|
203
|
+
connectionManager: deps.clientConnectionManager,
|
|
204
|
+
});
|
|
205
|
+
registerWorktreeRoutes(app, { workspaceManager: deps.workspaceManager });
|
|
206
|
+
registerStorageRoutes(app, {
|
|
207
|
+
instanceStore: deps.instanceStore,
|
|
208
|
+
eventBus: deps.eventBus,
|
|
209
|
+
workspaceManager: deps.workspaceManager,
|
|
210
|
+
});
|
|
211
|
+
registerRemoteServerRoutes(app, { logger: apiLogger });
|
|
212
|
+
registerRemoteProxyRoutes(app, { logger: proxyLogger, sessionManager: deps.remoteProxySessionManager });
|
|
213
|
+
registerSpeechRoutes(app, { speechService: deps.speechService });
|
|
214
|
+
registerSideCarRoutes(app, { sidecarManager: deps.sidecarManager });
|
|
215
|
+
registerSideCarProxyRoutes(app, { sidecarManager: deps.sidecarManager, logger: proxyLogger });
|
|
216
|
+
setupSideCarWebSocketProxy(app, {
|
|
217
|
+
sidecarManager: deps.sidecarManager,
|
|
218
|
+
authManager: deps.authManager,
|
|
219
|
+
logger: proxyLogger,
|
|
220
|
+
});
|
|
221
|
+
registerPluginRoutes(app, {
|
|
222
|
+
workspaceManager: deps.workspaceManager,
|
|
223
|
+
eventBus: deps.eventBus,
|
|
224
|
+
logger: proxyLogger,
|
|
225
|
+
channel: deps.pluginChannel,
|
|
226
|
+
voiceModeManager: deps.voiceModeManager,
|
|
227
|
+
});
|
|
228
|
+
registerBackgroundProcessRoutes(app, { backgroundProcessManager });
|
|
229
|
+
registerInstanceProxyRoutes(app, { workspaceManager: deps.workspaceManager, logger: proxyLogger });
|
|
230
|
+
if (deps.uiDevServerUrl) {
|
|
231
|
+
setupDevProxy(app, deps.uiDevServerUrl, deps.authManager);
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
setupStaticUi(app, deps.uiStaticDir, deps.authManager);
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
instance: app,
|
|
238
|
+
start: async () => {
|
|
239
|
+
const attemptListen = async (requestedPort) => {
|
|
240
|
+
const addressInfo = await app.listen({ port: requestedPort, host: deps.bindHost });
|
|
241
|
+
return { addressInfo, requestedPort };
|
|
242
|
+
};
|
|
243
|
+
const autoPortRequested = deps.bindPort === 0;
|
|
244
|
+
const primaryPort = autoPortRequested ? deps.defaultPort : deps.bindPort;
|
|
245
|
+
const shouldRetryWithEphemeral = (error) => {
|
|
246
|
+
if (!autoPortRequested)
|
|
247
|
+
return false;
|
|
248
|
+
const err = error;
|
|
249
|
+
return Boolean(err && err.code === "EADDRINUSE");
|
|
250
|
+
};
|
|
251
|
+
let listenResult;
|
|
252
|
+
try {
|
|
253
|
+
listenResult = await attemptListen(primaryPort);
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
if (!shouldRetryWithEphemeral(error)) {
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
deps.logger.warn({ err: error, port: primaryPort }, "Preferred port unavailable, retrying on ephemeral port");
|
|
260
|
+
listenResult = await attemptListen(0);
|
|
261
|
+
}
|
|
262
|
+
let actualPort = listenResult.requestedPort;
|
|
263
|
+
if (typeof listenResult.addressInfo === "string") {
|
|
264
|
+
try {
|
|
265
|
+
const parsed = new URL(listenResult.addressInfo);
|
|
266
|
+
actualPort = Number(parsed.port) || listenResult.requestedPort;
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
actualPort = listenResult.requestedPort;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
const address = app.server.address();
|
|
274
|
+
if (typeof address === "object" && address) {
|
|
275
|
+
actualPort = address.port;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const displayHost = deps.bindHost === "127.0.0.1" ? "localhost" : deps.bindHost;
|
|
279
|
+
const serverUrl = `${deps.protocol}://${displayHost}:${actualPort}`;
|
|
280
|
+
deps.logger.info({ port: actualPort, host: deps.bindHost, protocol: deps.protocol }, "HTTP server listening");
|
|
281
|
+
return { port: actualPort, url: serverUrl, displayHost };
|
|
282
|
+
},
|
|
283
|
+
stop: () => {
|
|
284
|
+
closeSseClients();
|
|
285
|
+
return app.close();
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function registerSideCarProxyRoutes(app, deps) {
|
|
290
|
+
const proxyBaseHandler = async (request, reply) => {
|
|
291
|
+
await proxySideCarRequest({
|
|
292
|
+
request,
|
|
293
|
+
reply,
|
|
294
|
+
sidecarManager: deps.sidecarManager,
|
|
295
|
+
logger: deps.logger,
|
|
296
|
+
pathSuffix: "",
|
|
297
|
+
});
|
|
298
|
+
};
|
|
299
|
+
const proxyWildcardHandler = async (request, reply) => {
|
|
300
|
+
await proxySideCarRequest({
|
|
301
|
+
request,
|
|
302
|
+
reply,
|
|
303
|
+
sidecarManager: deps.sidecarManager,
|
|
304
|
+
logger: deps.logger,
|
|
305
|
+
pathSuffix: request.params["*"] ?? "",
|
|
306
|
+
});
|
|
307
|
+
};
|
|
308
|
+
app.all("/sidecars/:id", proxyBaseHandler);
|
|
309
|
+
app.all("/sidecars/:id/*", proxyWildcardHandler);
|
|
310
|
+
}
|
|
311
|
+
function setupSideCarWebSocketProxy(app, deps) {
|
|
312
|
+
app.server.on("upgrade", (request, socket, head) => {
|
|
313
|
+
const rawUrl = request.url ?? "/";
|
|
314
|
+
const parsed = parseSideCarUpgradePath(rawUrl);
|
|
315
|
+
if (!parsed) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
void proxySideCarWebSocketUpgrade({
|
|
319
|
+
request,
|
|
320
|
+
socket: socket,
|
|
321
|
+
head,
|
|
322
|
+
sidecarId: parsed.sidecarId,
|
|
323
|
+
incomingPath: parsed.pathname,
|
|
324
|
+
search: parsed.search,
|
|
325
|
+
sidecarManager: deps.sidecarManager,
|
|
326
|
+
authManager: deps.authManager,
|
|
327
|
+
logger: deps.logger,
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
function registerInstanceProxyRoutes(app, deps) {
|
|
332
|
+
app.register(async (instance) => {
|
|
333
|
+
instance.removeAllContentTypeParsers();
|
|
334
|
+
instance.addContentTypeParser("*", (req, body, done) => done(null, body));
|
|
335
|
+
const proxyBaseHandler = async (request, reply) => {
|
|
336
|
+
await proxyWorkspaceRequest({
|
|
337
|
+
request,
|
|
338
|
+
reply,
|
|
339
|
+
workspaceManager: deps.workspaceManager,
|
|
340
|
+
worktreeSlug: request.params.slug,
|
|
341
|
+
pathSuffix: "",
|
|
342
|
+
logger: deps.logger,
|
|
343
|
+
});
|
|
344
|
+
};
|
|
345
|
+
const proxyWildcardHandler = async (request, reply) => {
|
|
346
|
+
await proxyWorkspaceRequest({
|
|
347
|
+
request,
|
|
348
|
+
reply,
|
|
349
|
+
workspaceManager: deps.workspaceManager,
|
|
350
|
+
worktreeSlug: request.params.slug,
|
|
351
|
+
pathSuffix: request.params["*"] ?? "",
|
|
352
|
+
logger: deps.logger,
|
|
353
|
+
});
|
|
354
|
+
};
|
|
355
|
+
instance.all("/workspaces/:id/worktrees/:slug/instance", proxyBaseHandler);
|
|
356
|
+
instance.all("/workspaces/:id/worktrees/:slug/instance/*", proxyWildcardHandler);
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
const INSTANCE_PROXY_HOST = "127.0.0.1";
|
|
360
|
+
// Special-case OpenCode directory override.
|
|
361
|
+
//
|
|
362
|
+
// UI clients may need to scope certain requests to an arbitrary directory that is not
|
|
363
|
+
// part of the Git worktree list. Since the OpenCode SDK does not reliably support
|
|
364
|
+
// injecting per-request headers, we encode an override into the *path* and strip it
|
|
365
|
+
// before proxying to the instance.
|
|
366
|
+
//
|
|
367
|
+
// Example proxied request path:
|
|
368
|
+
// /workspaces/:id/worktrees/:slug/instance/__dir/<base64url>/session/create
|
|
369
|
+
//
|
|
370
|
+
// The server will decode <base64url> -> absolute directory, validate it, then set
|
|
371
|
+
// x-opencode-directory accordingly and forward the request to /session/create.
|
|
372
|
+
const OPENCODE_DIR_OVERRIDE_PREFIX = "__dir/";
|
|
373
|
+
const OPENCODE_DIR_OVERRIDE_MAX_LEN = 4096;
|
|
374
|
+
async function proxyWorkspaceRequest(args) {
|
|
375
|
+
const { request, reply, workspaceManager, logger, worktreeSlug } = args;
|
|
376
|
+
const workspaceId = request.params.id;
|
|
377
|
+
const workspace = workspaceManager.get(workspaceId);
|
|
378
|
+
const bodyToJson = (body) => {
|
|
379
|
+
if (body == null)
|
|
380
|
+
return null;
|
|
381
|
+
const anyBody = body;
|
|
382
|
+
if (anyBody && typeof anyBody.pipe === "function") {
|
|
383
|
+
// Don't consume streams (would break proxying).
|
|
384
|
+
// Best-effort: if the stream already has buffered chunks, parse those.
|
|
385
|
+
try {
|
|
386
|
+
const buffered = anyBody?._readableState?.buffer;
|
|
387
|
+
if (Array.isArray(buffered) && buffered.length > 0) {
|
|
388
|
+
const chunks = [];
|
|
389
|
+
for (const entry of buffered) {
|
|
390
|
+
if (!entry)
|
|
391
|
+
continue;
|
|
392
|
+
if (Buffer.isBuffer(entry)) {
|
|
393
|
+
chunks.push(entry);
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
const data = entry.data;
|
|
397
|
+
if (Buffer.isBuffer(data)) {
|
|
398
|
+
chunks.push(data);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (chunks.length > 0) {
|
|
402
|
+
const text = Buffer.concat(chunks).toString("utf-8");
|
|
403
|
+
try {
|
|
404
|
+
return JSON.parse(text);
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
return { __raw: text };
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
// fall through
|
|
414
|
+
}
|
|
415
|
+
return { __stream: true };
|
|
416
|
+
}
|
|
417
|
+
const maybeParse = (input) => {
|
|
418
|
+
try {
|
|
419
|
+
return JSON.parse(input);
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
return { __raw: input };
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
if (Buffer.isBuffer(body)) {
|
|
426
|
+
return maybeParse(body.toString("utf-8"));
|
|
427
|
+
}
|
|
428
|
+
if (typeof body === "string") {
|
|
429
|
+
return maybeParse(body);
|
|
430
|
+
}
|
|
431
|
+
if (typeof body === "object") {
|
|
432
|
+
return body;
|
|
433
|
+
}
|
|
434
|
+
return body;
|
|
435
|
+
};
|
|
436
|
+
if (!workspace) {
|
|
437
|
+
reply.code(404).send({ error: "Workspace not found" });
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const port = workspaceManager.getInstancePort(workspaceId);
|
|
441
|
+
if (!port) {
|
|
442
|
+
reply.code(502).send({ error: "Workspace instance is not ready" });
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (!isValidWorktreeSlug(worktreeSlug)) {
|
|
446
|
+
reply.code(400).send({ error: "Invalid worktree slug" });
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
let extracted;
|
|
450
|
+
try {
|
|
451
|
+
extracted = extractOpencodeDirectoryOverride(args.pathSuffix);
|
|
452
|
+
}
|
|
453
|
+
catch (error) {
|
|
454
|
+
const message = error instanceof Error ? error.message : "Invalid directory override";
|
|
455
|
+
reply.code(400).send({ error: message });
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
let directory = null;
|
|
459
|
+
let forwardedSuffix = extracted.forwardedSuffix;
|
|
460
|
+
if (extracted.overrideDirectory) {
|
|
461
|
+
try {
|
|
462
|
+
directory = validateAndNormalizeOverrideDirectory({
|
|
463
|
+
overrideDirectory: extracted.overrideDirectory,
|
|
464
|
+
workspaceRoot: workspace.path,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
const message = error instanceof Error ? error.message : "Invalid directory override";
|
|
469
|
+
reply.code(400).send({ error: message });
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
directory = await resolveWorktreeDirectory({
|
|
475
|
+
workspaceId,
|
|
476
|
+
workspacePath: workspace.path,
|
|
477
|
+
worktreeSlug,
|
|
478
|
+
logger,
|
|
479
|
+
});
|
|
480
|
+
if (!directory) {
|
|
481
|
+
reply.code(404).send({ error: "Worktree not found" });
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
const normalizedSuffix = normalizeInstanceSuffix(forwardedSuffix);
|
|
486
|
+
const queryIndex = (request.raw.url ?? "").indexOf("?");
|
|
487
|
+
const search = queryIndex >= 0 ? (request.raw.url ?? "").slice(queryIndex) : "";
|
|
488
|
+
const targetUrl = `http://${INSTANCE_PROXY_HOST}:${port}${normalizedSuffix}${search}`;
|
|
489
|
+
const instanceAuthHeader = workspaceManager.getInstanceAuthorizationHeader(workspaceId);
|
|
490
|
+
logger.debug({ workspaceId, method: request.method, targetUrl }, "Proxying request to instance");
|
|
491
|
+
if (logger.isLevelEnabled("trace")) {
|
|
492
|
+
logger.trace({ workspaceId, targetUrl, body: request.body }, "Instance proxy payload");
|
|
493
|
+
}
|
|
494
|
+
const headers = buildWorkspaceInstanceProxyHeaders(request.headers, instanceAuthHeader, directory);
|
|
495
|
+
if (logger.isLevelEnabled("trace")) {
|
|
496
|
+
logger.trace({
|
|
497
|
+
workspaceId,
|
|
498
|
+
method: request.method,
|
|
499
|
+
targetUrl,
|
|
500
|
+
worktreeSlug,
|
|
501
|
+
directory,
|
|
502
|
+
contentType: request.headers["content-type"],
|
|
503
|
+
body: bodyToJson(request.body),
|
|
504
|
+
headers: redactProxyHeadersForLogs(headers),
|
|
505
|
+
}, "Proxy -> OpenCode request");
|
|
506
|
+
}
|
|
507
|
+
const init = {
|
|
508
|
+
method: request.method,
|
|
509
|
+
headers,
|
|
510
|
+
redirect: "manual",
|
|
511
|
+
};
|
|
512
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
513
|
+
const body = toProxyRequestBody(request.body);
|
|
514
|
+
if (body !== undefined) {
|
|
515
|
+
init.body = body;
|
|
516
|
+
init.duplex = "half";
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
try {
|
|
520
|
+
const response = await fetch(targetUrl, init);
|
|
521
|
+
reply.code(response.status);
|
|
522
|
+
applyInstanceProxyResponseHeaders(reply, response);
|
|
523
|
+
if (!response.body || request.method === "HEAD") {
|
|
524
|
+
reply.send();
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
reply.hijack();
|
|
528
|
+
reply.raw.writeHead(reply.statusCode, toOutgoingHeaders(reply.getHeaders()));
|
|
529
|
+
await pipeline(Readable.fromWeb(response.body), reply.raw);
|
|
530
|
+
}
|
|
531
|
+
catch (error) {
|
|
532
|
+
logger.error({ err: error, workspaceId, targetUrl }, "Failed to proxy workspace request");
|
|
533
|
+
if (!reply.sent) {
|
|
534
|
+
reply.code(502).send({ error: "Workspace instance proxy failed" });
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
function extractOpencodeDirectoryOverride(pathSuffix) {
|
|
539
|
+
if (!pathSuffix) {
|
|
540
|
+
return { overrideDirectory: null, forwardedSuffix: pathSuffix };
|
|
541
|
+
}
|
|
542
|
+
// Fastify wildcard param does not include a leading slash.
|
|
543
|
+
const trimmed = pathSuffix.replace(/^\/+/, "");
|
|
544
|
+
if (!trimmed.startsWith(OPENCODE_DIR_OVERRIDE_PREFIX)) {
|
|
545
|
+
return { overrideDirectory: null, forwardedSuffix: pathSuffix };
|
|
546
|
+
}
|
|
547
|
+
const rest = trimmed.slice(OPENCODE_DIR_OVERRIDE_PREFIX.length);
|
|
548
|
+
const slashIndex = rest.indexOf("/");
|
|
549
|
+
const encoded = (slashIndex >= 0 ? rest.slice(0, slashIndex) : rest).trim();
|
|
550
|
+
const remaining = slashIndex >= 0 ? rest.slice(slashIndex + 1) : "";
|
|
551
|
+
if (!encoded) {
|
|
552
|
+
throw new Error("Missing directory override");
|
|
553
|
+
}
|
|
554
|
+
if (encoded.length > OPENCODE_DIR_OVERRIDE_MAX_LEN) {
|
|
555
|
+
throw new Error("Directory override too large");
|
|
556
|
+
}
|
|
557
|
+
let overrideDirectory = "";
|
|
558
|
+
try {
|
|
559
|
+
overrideDirectory = decodeBase64Url(encoded);
|
|
560
|
+
}
|
|
561
|
+
catch {
|
|
562
|
+
throw new Error("Invalid directory override");
|
|
563
|
+
}
|
|
564
|
+
const forwardedSuffix = remaining;
|
|
565
|
+
return { overrideDirectory, forwardedSuffix };
|
|
566
|
+
}
|
|
567
|
+
function decodeBase64Url(input) {
|
|
568
|
+
// base64url -> base64
|
|
569
|
+
const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
570
|
+
const padding = normalized.length % 4 === 0 ? "" : "=".repeat(4 - (normalized.length % 4));
|
|
571
|
+
const base64 = `${normalized}${padding}`;
|
|
572
|
+
return Buffer.from(base64, "base64").toString("utf-8");
|
|
573
|
+
}
|
|
574
|
+
function validateAndNormalizeOverrideDirectory(params) {
|
|
575
|
+
const raw = params.overrideDirectory.trim();
|
|
576
|
+
if (!raw) {
|
|
577
|
+
throw new Error("Override directory is empty");
|
|
578
|
+
}
|
|
579
|
+
if (!path.isAbsolute(raw)) {
|
|
580
|
+
throw new Error("Override directory must be an absolute path");
|
|
581
|
+
}
|
|
582
|
+
if (!fs.existsSync(raw)) {
|
|
583
|
+
throw new Error(`Override directory does not exist: ${raw}`);
|
|
584
|
+
}
|
|
585
|
+
const stats = fs.statSync(raw);
|
|
586
|
+
if (!stats.isDirectory()) {
|
|
587
|
+
throw new Error(`Override path is not a directory: ${raw}`);
|
|
588
|
+
}
|
|
589
|
+
const normalizedOverride = fs.realpathSync(raw);
|
|
590
|
+
const normalizedRoot = fs.realpathSync(params.workspaceRoot);
|
|
591
|
+
if (!isSubpath(normalizedOverride, normalizedRoot)) {
|
|
592
|
+
throw new Error("Override directory must be within the workspace root");
|
|
593
|
+
}
|
|
594
|
+
return normalizedOverride;
|
|
595
|
+
}
|
|
596
|
+
function isSubpath(candidate, root) {
|
|
597
|
+
const rel = path.relative(root, candidate);
|
|
598
|
+
if (rel === "")
|
|
599
|
+
return true;
|
|
600
|
+
if (rel === "..")
|
|
601
|
+
return false;
|
|
602
|
+
if (rel.startsWith(`..${path.sep}`))
|
|
603
|
+
return false;
|
|
604
|
+
if (path.isAbsolute(rel))
|
|
605
|
+
return false;
|
|
606
|
+
return true;
|
|
607
|
+
}
|
|
608
|
+
function normalizeInstanceSuffix(pathSuffix) {
|
|
609
|
+
if (!pathSuffix || pathSuffix === "/") {
|
|
610
|
+
return "/";
|
|
611
|
+
}
|
|
612
|
+
const trimmed = pathSuffix.replace(/^\/+/, "");
|
|
613
|
+
return trimmed.length === 0 ? "/" : `/${trimmed}`;
|
|
614
|
+
}
|
|
615
|
+
function setupStaticUi(app, uiDir, authManager) {
|
|
616
|
+
if (!uiDir) {
|
|
617
|
+
app.log.warn("UI static directory not provided; API endpoints only");
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
if (!fs.existsSync(uiDir)) {
|
|
621
|
+
app.log.warn({ uiDir }, "UI static directory missing; API endpoints only");
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
app.register(fastifyStatic, {
|
|
625
|
+
root: uiDir,
|
|
626
|
+
prefix: "/",
|
|
627
|
+
decorateReply: false,
|
|
628
|
+
});
|
|
629
|
+
const indexPath = path.join(uiDir, "index.html");
|
|
630
|
+
app.setNotFoundHandler((request, reply) => {
|
|
631
|
+
const url = request.raw.url ?? "";
|
|
632
|
+
if (isApiRequest(url)) {
|
|
633
|
+
reply.code(404).send({ message: "Not Found" });
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
const session = authManager.getSessionFromRequest(request);
|
|
637
|
+
if (!session && wantsHtml(request)) {
|
|
638
|
+
reply.redirect("/login");
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (fs.existsSync(indexPath)) {
|
|
642
|
+
reply.type("text/html").send(fs.readFileSync(indexPath, "utf-8"));
|
|
643
|
+
}
|
|
644
|
+
else {
|
|
645
|
+
reply.code(404).send({ message: "UI bundle missing" });
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
function setupDevProxy(app, upstreamBase, authManager) {
|
|
650
|
+
app.log.info({ upstreamBase }, "Proxying UI requests to development server");
|
|
651
|
+
app.setNotFoundHandler((request, reply) => {
|
|
652
|
+
const url = request.raw.url ?? "";
|
|
653
|
+
if (isApiRequest(url)) {
|
|
654
|
+
reply.code(404).send({ message: "Not Found" });
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
const session = authManager.getSessionFromRequest(request);
|
|
658
|
+
if (!session && wantsHtml(request)) {
|
|
659
|
+
reply.redirect("/login");
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
void proxyToDevServer(request, reply, upstreamBase);
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
async function proxyToDevServer(request, reply, upstreamBase) {
|
|
666
|
+
try {
|
|
667
|
+
const targetUrl = new URL(request.raw.url ?? "/", upstreamBase);
|
|
668
|
+
const response = await fetch(targetUrl, {
|
|
669
|
+
method: request.method,
|
|
670
|
+
headers: buildProxyHeaders(request.headers),
|
|
671
|
+
});
|
|
672
|
+
response.headers.forEach((value, key) => {
|
|
673
|
+
reply.header(key, value);
|
|
674
|
+
});
|
|
675
|
+
reply.code(response.status);
|
|
676
|
+
if (!response.body || request.method === "HEAD") {
|
|
677
|
+
reply.send();
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
681
|
+
reply.send(buffer);
|
|
682
|
+
}
|
|
683
|
+
catch (error) {
|
|
684
|
+
request.log.error({ err: error }, "Failed to proxy UI request to dev server");
|
|
685
|
+
if (!reply.sent) {
|
|
686
|
+
reply.code(502).send("UI dev server is unavailable");
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
function isApiRequest(rawUrl) {
|
|
691
|
+
if (!rawUrl)
|
|
692
|
+
return false;
|
|
693
|
+
const pathname = rawUrl.split("?")[0] ?? "";
|
|
694
|
+
return pathname === "/api" || pathname.startsWith("/api/");
|
|
695
|
+
}
|
|
696
|
+
function buildProxyHeaders(headers) {
|
|
697
|
+
const result = {};
|
|
698
|
+
for (const [key, value] of Object.entries(headers ?? {})) {
|
|
699
|
+
const lower = key.toLowerCase();
|
|
700
|
+
if (!value || lower === "host" || isHopByHopHeader(lower))
|
|
701
|
+
continue;
|
|
702
|
+
result[key] = Array.isArray(value) ? value.join(",") : value;
|
|
703
|
+
}
|
|
704
|
+
return result;
|
|
705
|
+
}
|
|
706
|
+
function toProxyRequestBody(body) {
|
|
707
|
+
if (body == null) {
|
|
708
|
+
return undefined;
|
|
709
|
+
}
|
|
710
|
+
if (typeof body.pipe === "function") {
|
|
711
|
+
return body;
|
|
712
|
+
}
|
|
713
|
+
if (typeof body[Symbol.asyncIterator] === "function") {
|
|
714
|
+
return body;
|
|
715
|
+
}
|
|
716
|
+
if (Buffer.isBuffer(body) || typeof body === "string" || body instanceof Uint8Array) {
|
|
717
|
+
return body;
|
|
718
|
+
}
|
|
719
|
+
return JSON.stringify(body);
|
|
720
|
+
}
|
|
721
|
+
function buildWorkspaceInstanceProxyHeaders(headers, instanceAuthHeader, directory) {
|
|
722
|
+
const next = buildProxyHeaders(headers);
|
|
723
|
+
if (instanceAuthHeader) {
|
|
724
|
+
next.authorization = instanceAuthHeader;
|
|
725
|
+
}
|
|
726
|
+
const isNonASCII = /[^\x00-\x7F]/.test(directory);
|
|
727
|
+
next["x-opencode-directory"] = isNonASCII ? encodeURIComponent(directory) : directory;
|
|
728
|
+
return next;
|
|
729
|
+
}
|
|
730
|
+
function redactProxyHeadersForLogs(headers) {
|
|
731
|
+
const outgoing = { ...headers };
|
|
732
|
+
for (const key of Object.keys(outgoing)) {
|
|
733
|
+
const lower = key.toLowerCase();
|
|
734
|
+
if (lower === "authorization" || lower === "cookie" || lower === "set-cookie") {
|
|
735
|
+
outgoing[key] = "<redacted>";
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return outgoing;
|
|
739
|
+
}
|
|
740
|
+
function applyInstanceProxyResponseHeaders(reply, response) {
|
|
741
|
+
response.headers.forEach((value, key) => {
|
|
742
|
+
const lower = key.toLowerCase();
|
|
743
|
+
if (isHopByHopHeader(lower) || lower === "content-length" || lower === "content-encoding") {
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
reply.header(key, value);
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
function toOutgoingHeaders(headers) {
|
|
750
|
+
const next = {};
|
|
751
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
752
|
+
if (value === undefined) {
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
next[key] = Array.isArray(value) ? value.map(String) : String(value);
|
|
756
|
+
}
|
|
757
|
+
return next;
|
|
758
|
+
}
|
|
759
|
+
function isHopByHopHeader(name) {
|
|
760
|
+
return new Set([
|
|
761
|
+
"connection",
|
|
762
|
+
"keep-alive",
|
|
763
|
+
"proxy-authenticate",
|
|
764
|
+
"proxy-authorization",
|
|
765
|
+
"te",
|
|
766
|
+
"trailer",
|
|
767
|
+
"transfer-encoding",
|
|
768
|
+
"upgrade",
|
|
769
|
+
]).has(name);
|
|
770
|
+
}
|
|
771
|
+
async function proxySideCarRequest(args) {
|
|
772
|
+
const sidecarId = args.request.params.id ?? "";
|
|
773
|
+
const sidecar = await args.sidecarManager.get(sidecarId);
|
|
774
|
+
if (!sidecar) {
|
|
775
|
+
args.reply.code(404).send({ error: "SideCar not found" });
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
const pathname = (args.request.raw.url ?? args.request.url ?? "").split("?")[0] ?? "";
|
|
779
|
+
const queryIndex = (args.request.raw.url ?? args.request.url ?? "").indexOf("?");
|
|
780
|
+
const search = queryIndex >= 0 ? (args.request.raw.url ?? args.request.url ?? "").slice(queryIndex) : "";
|
|
781
|
+
const pathSuffix = args.pathSuffix ?? "";
|
|
782
|
+
const requestPath = pathSuffix ? `${args.sidecarManager.buildProxyBasePath(sidecarId)}/${pathSuffix.replace(/^\/+/, "")}` : args.sidecarManager.buildProxyBasePath(sidecarId);
|
|
783
|
+
const targetPath = args.sidecarManager.buildTargetPath(sidecarId, requestPath, search);
|
|
784
|
+
const targetOrigin = args.sidecarManager.buildTargetOrigin(sidecar);
|
|
785
|
+
const targetUrl = `${targetOrigin}${targetPath}`;
|
|
786
|
+
args.logger.debug({ sidecarId: sidecar.id, targetUrl, pathname, prefixMode: sidecar.prefixMode }, "Proxying request to SideCar");
|
|
787
|
+
await args.reply.from(targetUrl, {
|
|
788
|
+
rewriteRequestHeaders: (_originalRequest, headers) => sanitizeSideCarProxyRequestHeaders(headers, targetOrigin),
|
|
789
|
+
rewriteHeaders: (headers) => rewriteSideCarResponseHeaders(headers, sidecarId, targetOrigin, sidecar.prefixMode),
|
|
790
|
+
onError: (reply, { error }) => {
|
|
791
|
+
args.logger.error({ sidecarId: sidecar.id, err: error, targetUrl }, "Failed to proxy SideCar request");
|
|
792
|
+
if (!reply.sent) {
|
|
793
|
+
reply.code(502).send({ error: "SideCar proxy failed" });
|
|
794
|
+
}
|
|
795
|
+
},
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
function parseSideCarUpgradePath(rawUrl) {
|
|
799
|
+
let parsed;
|
|
800
|
+
try {
|
|
801
|
+
parsed = new URL(rawUrl, "http://localhost");
|
|
802
|
+
}
|
|
803
|
+
catch {
|
|
804
|
+
return null;
|
|
805
|
+
}
|
|
806
|
+
const match = parsed.pathname.match(/^\/sidecars\/([^/]+)(?:\/.*)?$/);
|
|
807
|
+
if (!match) {
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
try {
|
|
811
|
+
return {
|
|
812
|
+
sidecarId: decodeURIComponent(match[1] ?? ""),
|
|
813
|
+
pathname: parsed.pathname,
|
|
814
|
+
search: parsed.search,
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
catch {
|
|
818
|
+
return null;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
async function proxySideCarWebSocketUpgrade(args) {
|
|
822
|
+
const { request, socket, head, sidecarId, incomingPath, search, sidecarManager, authManager, logger } = args;
|
|
823
|
+
if (!isWebSocketUpgradeRequest(request)) {
|
|
824
|
+
rejectUpgrade(socket, 400, "Bad Request");
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
const session = authManager.getSessionFromHeaders(request.headers);
|
|
828
|
+
if (!session) {
|
|
829
|
+
rejectUpgrade(socket, 401, "Unauthorized");
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
const sidecar = await sidecarManager.get(sidecarId);
|
|
833
|
+
if (!sidecar) {
|
|
834
|
+
rejectUpgrade(socket, 404, "Not Found");
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
const targetOrigin = sidecarManager.buildTargetOrigin(sidecar);
|
|
838
|
+
const targetPath = sidecarManager.buildTargetPath(sidecarId, incomingPath, search);
|
|
839
|
+
const targetUrl = new URL(`${targetOrigin}${targetPath}`);
|
|
840
|
+
logger.debug({ sidecarId, targetUrl: targetUrl.toString(), prefixMode: sidecar.prefixMode }, "Proxying websocket to SideCar");
|
|
841
|
+
const { socket: upstream, readyEvent } = createSideCarUpstreamSocket(targetUrl);
|
|
842
|
+
const closeBoth = () => {
|
|
843
|
+
if (!socket.destroyed) {
|
|
844
|
+
socket.destroy();
|
|
845
|
+
}
|
|
846
|
+
if (!upstream.destroyed) {
|
|
847
|
+
upstream.destroy();
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
upstream.once("error", (error) => {
|
|
851
|
+
logger.error({ sidecarId, err: error, targetUrl: targetUrl.toString() }, "Failed to proxy SideCar websocket");
|
|
852
|
+
rejectUpgrade(socket, 502, "Bad Gateway");
|
|
853
|
+
if (!upstream.destroyed) {
|
|
854
|
+
upstream.destroy();
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
socket.once("error", (error) => {
|
|
858
|
+
logger.debug({ sidecarId, err: error }, "SideCar websocket client socket errored");
|
|
859
|
+
if (!upstream.destroyed) {
|
|
860
|
+
upstream.destroy();
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
upstream.once(readyEvent, () => {
|
|
864
|
+
try {
|
|
865
|
+
upstream.write(buildSideCarWebSocketRequest(request, targetUrl));
|
|
866
|
+
if (head.length > 0) {
|
|
867
|
+
upstream.write(head);
|
|
868
|
+
}
|
|
869
|
+
upstream.pipe(socket);
|
|
870
|
+
socket.pipe(upstream);
|
|
871
|
+
}
|
|
872
|
+
catch (error) {
|
|
873
|
+
logger.error({ sidecarId, err: error, targetUrl: targetUrl.toString() }, "Failed to forward SideCar websocket upgrade");
|
|
874
|
+
closeBoth();
|
|
875
|
+
}
|
|
876
|
+
});
|
|
877
|
+
upstream.once("close", () => {
|
|
878
|
+
if (!socket.destroyed) {
|
|
879
|
+
socket.end();
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
socket.once("close", () => {
|
|
883
|
+
if (!upstream.destroyed) {
|
|
884
|
+
upstream.end();
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
function createSideCarUpstreamSocket(targetUrl) {
|
|
889
|
+
const port = Number(targetUrl.port || (targetUrl.protocol === "https:" ? 443 : 80));
|
|
890
|
+
if (targetUrl.protocol === "https:") {
|
|
891
|
+
return {
|
|
892
|
+
socket: connectTls({
|
|
893
|
+
host: targetUrl.hostname,
|
|
894
|
+
port,
|
|
895
|
+
servername: targetUrl.hostname,
|
|
896
|
+
}),
|
|
897
|
+
readyEvent: "secureConnect",
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
return {
|
|
901
|
+
socket: connectTcp(port, targetUrl.hostname),
|
|
902
|
+
readyEvent: "connect",
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
function buildSideCarWebSocketRequest(request, targetUrl) {
|
|
906
|
+
const pathWithQuery = `${targetUrl.pathname}${targetUrl.search}`;
|
|
907
|
+
const requestLine = `${request.method ?? "GET"} ${pathWithQuery} HTTP/${request.httpVersion}\r\n`;
|
|
908
|
+
const headerLines = [];
|
|
909
|
+
const rawHeaders = request.rawHeaders ?? [];
|
|
910
|
+
const blockedHeaders = getBlockedSideCarRequestHeaders();
|
|
911
|
+
for (let index = 0; index < rawHeaders.length; index += 2) {
|
|
912
|
+
const key = rawHeaders[index];
|
|
913
|
+
const value = rawHeaders[index + 1];
|
|
914
|
+
if (!key || value === undefined)
|
|
915
|
+
continue;
|
|
916
|
+
const lower = key.toLowerCase();
|
|
917
|
+
if (blockedHeaders.has(lower))
|
|
918
|
+
continue;
|
|
919
|
+
if (lower === "origin") {
|
|
920
|
+
headerLines.push(`Origin: ${targetUrl.origin}\r\n`);
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
headerLines.push(`${key}: ${value}\r\n`);
|
|
924
|
+
}
|
|
925
|
+
const hostValue = targetUrl.port ? `${targetUrl.hostname}:${targetUrl.port}` : targetUrl.hostname;
|
|
926
|
+
headerLines.push(`Host: ${hostValue}\r\n`);
|
|
927
|
+
headerLines.push("\r\n");
|
|
928
|
+
return requestLine + headerLines.join("");
|
|
929
|
+
}
|
|
930
|
+
function isWebSocketUpgradeRequest(request) {
|
|
931
|
+
const upgrade = request.headers.upgrade;
|
|
932
|
+
if (typeof upgrade !== "string" || upgrade.toLowerCase() !== "websocket") {
|
|
933
|
+
return false;
|
|
934
|
+
}
|
|
935
|
+
const connection = request.headers.connection;
|
|
936
|
+
const connectionValue = Array.isArray(connection) ? connection.join(",") : connection ?? "";
|
|
937
|
+
return connectionValue.toLowerCase().split(",").map((part) => part.trim()).includes("upgrade");
|
|
938
|
+
}
|
|
939
|
+
function rejectUpgrade(socket, statusCode, statusText) {
|
|
940
|
+
if (socket.destroyed) {
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
socket.write(`HTTP/1.1 ${statusCode} ${statusText}\r\nConnection: close\r\nContent-Length: 0\r\n\r\n`);
|
|
944
|
+
socket.destroy();
|
|
945
|
+
}
|
|
946
|
+
function rewriteSideCarResponseHeaders(headers, sidecarId, targetOrigin, prefixMode) {
|
|
947
|
+
if (prefixMode === "preserve") {
|
|
948
|
+
return headers;
|
|
949
|
+
}
|
|
950
|
+
const next = { ...headers };
|
|
951
|
+
const locationHeader = next.location;
|
|
952
|
+
const location = Array.isArray(locationHeader) ? locationHeader[0] : locationHeader;
|
|
953
|
+
if (!location) {
|
|
954
|
+
return next;
|
|
955
|
+
}
|
|
956
|
+
const publicBase = `/sidecars/${encodeURIComponent(sidecarId)}`;
|
|
957
|
+
if (location.startsWith("/")) {
|
|
958
|
+
next.location = `${publicBase}${location}`;
|
|
959
|
+
return next;
|
|
960
|
+
}
|
|
961
|
+
try {
|
|
962
|
+
const parsed = new URL(location);
|
|
963
|
+
if (parsed.origin === targetOrigin) {
|
|
964
|
+
next.location = `${publicBase}${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
catch {
|
|
968
|
+
// Relative redirects should continue to resolve against the public sidecar path.
|
|
969
|
+
}
|
|
970
|
+
return next;
|
|
971
|
+
}
|
|
972
|
+
function sanitizeSideCarProxyRequestHeaders(headers, targetOrigin) {
|
|
973
|
+
const blockedHeaders = getBlockedSideCarRequestHeaders();
|
|
974
|
+
const next = {};
|
|
975
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
976
|
+
if (!value)
|
|
977
|
+
continue;
|
|
978
|
+
if (blockedHeaders.has(key.toLowerCase()))
|
|
979
|
+
continue;
|
|
980
|
+
next[key] = value;
|
|
981
|
+
}
|
|
982
|
+
next.origin = targetOrigin;
|
|
983
|
+
return next;
|
|
984
|
+
}
|
|
985
|
+
function getBlockedSideCarRequestHeaders() {
|
|
986
|
+
return new Set([
|
|
987
|
+
"host",
|
|
988
|
+
"authorization",
|
|
989
|
+
"proxy-authorization",
|
|
990
|
+
"forwarded",
|
|
991
|
+
"x-forwarded-for",
|
|
992
|
+
"x-forwarded-host",
|
|
993
|
+
"x-forwarded-port",
|
|
994
|
+
"x-forwarded-proto",
|
|
995
|
+
]);
|
|
996
|
+
}
|