@vantran-se/openchamber-web 1.23.1 → 1.24.3
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/bin/cli.test.js +29 -6
- package/bin/lib/cli-http.js +5 -1
- package/bin/lib/commands-status.js +5 -0
- package/dist/assets/{CodeMirrorCommentWidgets-BOpqXpFU.js → CodeMirrorCommentWidgets-BjJHrzmU.js} +1 -1
- package/dist/assets/DiffView-COVQhWGd.js +4 -0
- package/dist/assets/FilesView-B1V5JxFX.js +13 -0
- package/dist/assets/GitView-CDOJy5Ay.js +6 -0
- package/dist/assets/GuestAttachDialog-D3S0Rjic.js +1 -0
- package/dist/assets/{JsonTreeView-nd_E8eVM.js → JsonTreeView-D0C9nU7K.js} +1 -1
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/LinearIssuesView-D7FKg1XJ.js +1 -0
- package/dist/assets/{MarkdownImageGallery-CVYds3sQ.js → MarkdownImageGallery-BrFjAVEf.js} +1 -1
- package/dist/assets/MarkdownRendererImpl-DWepTafh.js +12 -0
- package/dist/assets/{OnboardingScreen-Dd2IbPTn.js → OnboardingScreen-Dyxxiqdt.js} +1 -1
- package/dist/assets/{PatchDiff-nUu5i4bG.js → PatchDiff-DnEhjXcC.js} +1 -1
- package/dist/assets/PierreDiffViewer-DyxZVVVR.js +210 -0
- package/dist/assets/PlanView-LW2z_EAP.js +5 -0
- package/dist/assets/PluginPane-B4tKno2h.js +3 -0
- package/dist/assets/SettingsView-DEaMulN9.js +29 -0
- package/dist/assets/{SettingsWindow-BccxY81J.js → SettingsWindow-FLDJXOgQ.js} +1 -1
- package/dist/assets/SubmoduleDiffSummary-CvVwD-Vi.js +1 -0
- package/dist/assets/ToolOutputDialog-BiP0x2Rw.js +15 -0
- package/dist/assets/{ToolPartDiffPreview-BFVgk0UK.js → ToolPartDiffPreview-CkR4msKP.js} +2 -2
- package/dist/assets/{VirtualizedFileDiff-z3_H74UR.js → VirtualizedFileDiff-0pyr_mHY.js} +3 -3
- package/dist/assets/{WalkthroughView-BqM9GFbJ.js → WalkthroughView-yNj4nOP5.js} +2 -2
- package/dist/assets/appThemeRegistry-BuUKhbYQ.js +1 -0
- package/dist/assets/commitAndPush-DvJKa3Ks.js +1 -0
- package/dist/assets/{computeVirtualFileMetrics-Bdazp9Qo.js → computeVirtualFileMetrics-DxTBMuu-.js} +1 -1
- package/dist/assets/de-DgxgFyGG.js +10 -0
- package/dist/assets/es-BFsZj5F_.js +16 -0
- package/dist/assets/fr-jdZ5KKQi.js +14 -0
- package/dist/assets/{index-eo1-D_7z.js → index-CKHjPKbq.js} +1 -1
- package/dist/assets/index-Cbci4i1W.js +40 -0
- package/dist/assets/index-DqaMWNrC.css +1 -0
- package/dist/assets/{isDiffPlainText-BvKH712-.js → isDiffPlainText-D7FkBMP6.js} +1 -1
- package/dist/assets/{isFilePlainText-BdVVPukf.js → isFilePlainText-CL2vsrIZ.js} +1 -1
- package/dist/assets/ja-C5kKrhO4.js +16 -0
- package/dist/assets/ko-DXDTT_0O.js +16 -0
- package/dist/assets/main-CZALtVek.js +896 -0
- package/dist/assets/main-CgXMBNvG.js +2 -0
- package/dist/assets/{markdown-shiki.worker-D5fBU-lw.js → markdown-shiki.worker-CahLvHIq.js} +5 -5
- package/dist/assets/markdownCore-Dh7WEsdM.js +5 -0
- package/dist/assets/miniChat-CwTmDixi.js +2 -0
- package/dist/assets/mobile-CLf2vCtO.js +2 -0
- package/dist/assets/{patchFileDiff-B7LjppSS.js → patchFileDiff-Bsba8KFp.js} +3 -3
- package/dist/assets/pl-D-7x4Y8Q.js +16 -0
- package/dist/assets/pt-BR-CSBZrHa8.js +16 -0
- package/dist/assets/renderElectronMiniChatApp-C5gAFBk0.js +2 -0
- package/dist/assets/renderMobileApp-V4hYd32H.js +25 -0
- package/dist/assets/runtimeEndpointReset-DhlHCn-2.js +25 -0
- package/dist/assets/{scheduledTasksApi-hceovDei.js → scheduledTasksApi-DzjbLd8P.js} +1 -1
- package/dist/assets/shikiHighlight-8_3csmr6.js +1 -0
- package/dist/assets/textMateThemeFromAppTheme-CHo2fCgW.js +1 -0
- package/dist/assets/tr-Dr-oJ22L.js +7 -0
- package/dist/assets/uk-9Nx83UcI.js +16 -0
- package/dist/assets/useAppFontEffects-khZXpFL_.js +7333 -0
- package/dist/assets/useInlineCommentController-DkQ_gqpn.js +2 -0
- package/dist/assets/useRootScrollLock-DdvHf9Pt.js +2 -0
- package/dist/assets/{vendor-base-ui-BY_5qla_.js → vendor-base-ui-CsXYnR5c.js} +1 -1
- package/dist/assets/{vendor-codemirror-language-data-DWA1bm1F.js → vendor-codemirror-language-data-CdzUmUyZ.js} +2 -2
- package/dist/assets/vendor-framer-motion-Bq3iTZ6r.js +1 -0
- package/dist/assets/vendor-katex-Cp579UoJ.css +1 -0
- package/dist/assets/{vendor-motion-dom-D_o3BTI3.js → vendor-motion-dom-DcCgeG69.js} +1 -1
- package/dist/assets/{vendor-remixicon-react-Dil0zKKT.js → vendor-remixicon-react-BDmGmFCA.js} +1 -1
- package/dist/assets/vendor-strip-json-comments-5dJlOkP1.js +4 -0
- package/dist/assets/{vendor-tanstack-react-virtual-g05Cx3z9.js → vendor-tanstack-react-virtual-ClYN8CzB.js} +1 -1
- package/dist/assets/{vendor-tanstack-virtual-core-Dubg10g5.js → vendor-tanstack-virtual-core-BewVQryB.js} +1 -1
- package/dist/assets/{vendor-zod-B6551dwG.js → vendor-zod-CgAdXWZO.js} +4 -4
- package/dist/assets/{web-components-B_Z64So5.js → web-components-B8IcVQAA.js} +2 -2
- package/dist/assets/zh-CN-Bxy_Hj3r.js +16 -0
- package/dist/assets/zh-TW-0BB4gpHM.js +16 -0
- package/dist/index.html +6 -4
- package/dist/mini-chat.html +6 -4
- package/dist/mobile.html +6 -4
- package/package.json +12 -9
- package/server/built-in-extensions/registry.json +4 -0
- package/server/index.d.ts +2 -0
- package/server/index.js +44 -10
- package/server/lib/agent-tool/DOCUMENTATION.md +17 -4
- package/server/lib/agent-tool/runtime.js +62 -15
- package/server/lib/agent-tool/runtime.test.js +88 -2
- package/server/lib/changelog/update-notes.js +2 -1
- package/server/lib/changelog/update-notes.test.js +19 -0
- package/server/lib/event-stream/DOCUMENTATION.md +23 -0
- package/server/lib/event-stream/delta-coalescer.js +0 -0
- package/server/lib/event-stream/delta-coalescer.test.js +292 -0
- package/server/lib/event-stream/global-hub.js +53 -23
- package/server/lib/event-stream/global-hub.test.js +160 -0
- package/server/lib/event-stream/global-ws-bridge.js +1 -0
- package/server/lib/event-stream/index.js +4 -0
- package/server/lib/fs/DOCUMENTATION.md +10 -1
- package/server/lib/fs/git-process.test.js +77 -0
- package/server/lib/fs/routes.js +58 -5
- package/server/lib/fs/routes.test.js +116 -0
- package/server/lib/fs/search.js +2 -1
- package/server/lib/git/DOCUMENTATION.md +20 -5
- package/server/lib/git/routes.js +16 -3
- package/server/lib/git/serial-refresh.js +93 -0
- package/server/lib/git/serial-refresh.test.js +112 -0
- package/server/lib/git/service.js +429 -70
- package/server/lib/git/service.test.js +418 -0
- package/server/lib/github/pr-status.js +5 -5
- package/server/lib/github/pr-status.test.js +1 -1
- package/server/lib/github/routes.js +4 -4
- package/server/lib/guests/DOCUMENTATION.md +68 -0
- package/server/lib/guests/auth-store.js +184 -0
- package/server/lib/guests/auth-store.test.js +112 -0
- package/server/lib/guests/background.test.js +83 -0
- package/server/lib/guests/builtins.js +34 -0
- package/server/lib/guests/builtins.test.js +174 -0
- package/server/lib/guests/catalog.js +477 -0
- package/server/lib/guests/catalog.test.js +390 -0
- package/server/lib/guests/clone.js +297 -0
- package/server/lib/guests/extract-zip.js +136 -0
- package/server/lib/guests/extract-zip.test.js +109 -0
- package/server/lib/guests/files.js +308 -0
- package/server/lib/guests/files.test.js +262 -0
- package/server/lib/guests/grant-scope.js +81 -0
- package/server/lib/guests/grant-scope.test.js +75 -0
- package/server/lib/guests/host-session.js +73 -0
- package/server/lib/guests/host-session.test.js +30 -0
- package/server/lib/guests/html-styles.js +6 -0
- package/server/lib/guests/html-styles.test.js +45 -0
- package/server/lib/guests/html-tokens.js +73 -0
- package/server/lib/guests/html-tokens.test.js +53 -0
- package/server/lib/guests/install.js +406 -0
- package/server/lib/guests/install.test.js +338 -0
- package/server/lib/guests/oauth.js +501 -0
- package/server/lib/guests/oauth.test.js +481 -0
- package/server/lib/guests/persist.js +336 -0
- package/server/lib/guests/persist.test.js +288 -0
- package/server/lib/guests/request.js +112 -0
- package/server/lib/guests/request.test.js +206 -0
- package/server/lib/guests/routes.js +838 -0
- package/server/lib/guests/service.js +538 -0
- package/server/lib/guests/service.test.js +277 -0
- package/server/lib/guests/sockets.js +129 -0
- package/server/lib/guests/sockets.test.js +110 -0
- package/server/lib/guests/ssh-install.test.js +188 -0
- package/server/lib/guests/storage.js +68 -0
- package/server/lib/guests/storage.test.js +59 -0
- package/server/lib/guests/updates.js +294 -0
- package/server/lib/guests/updates.test.js +334 -0
- package/server/lib/guests/upload.js +50 -0
- package/server/lib/guests/upload.test.js +131 -0
- package/server/lib/linear/DOCUMENTATION.md +1 -1
- package/server/lib/message-queue/DOCUMENTATION.md +10 -1
- package/server/lib/message-queue/runtime.js +21 -1
- package/server/lib/message-queue/runtime.test.js +52 -5
- package/server/lib/notifications/routes.js +5 -0
- package/server/lib/openchamber-control/DOCUMENTATION.md +6 -0
- package/server/lib/openchamber-sessions/routes.js +62 -29
- package/server/lib/openchamber-sessions/routes.test.js +96 -0
- package/server/lib/opencode/DOCUMENTATION.md +55 -1
- package/server/lib/opencode/bootstrap-runtime.js +1 -0
- package/server/lib/opencode/core-routes.js +39 -1
- package/server/lib/opencode/feature-routes-runtime.js +15 -0
- package/server/lib/opencode/lifecycle-process.test.js +84 -0
- package/server/lib/opencode/lifecycle.js +54 -67
- package/server/lib/opencode/openchamber-routes.js +72 -14
- package/server/lib/opencode/openchamber-routes.test.js +71 -3
- package/server/lib/opencode/session-runtime.js +68 -0
- package/server/lib/opencode/session-runtime.test.js +43 -0
- package/server/lib/opencode/settings-helpers.js +8 -0
- package/server/lib/opencode/settings-helpers.test.js +13 -1
- package/server/lib/opencode/settings-normalization-runtime.js +2 -0
- package/server/lib/opencode/settings-normalization-runtime.test.js +13 -0
- package/server/lib/opencode/settings-registry.json +15 -0
- package/server/lib/opencode/shutdown-runtime.js +3 -0
- package/server/lib/opencode/shutdown-runtime.test.js +24 -0
- package/server/lib/opencode/theme-archive.js +53 -0
- package/server/lib/opencode/theme-catalog.js +171 -0
- package/server/lib/opencode/theme-catalog.test.js +125 -0
- package/server/lib/opencode/theme-import.test.js +144 -0
- package/server/lib/opencode/theme-runtime.js +100 -28
- package/server/lib/opencode/theme-runtime.test.js +15 -0
- package/server/lib/permission-auto-accept/DOCUMENTATION.md +8 -0
- package/server/lib/permission-auto-accept/runtime.js +13 -0
- package/server/lib/permission-auto-accept/runtime.test.js +37 -1
- package/server/lib/projects/project-config.js +2 -2
- package/server/lib/quota/DOCUMENTATION.md +0 -1
- package/server/lib/quota/providers/index.js +0 -7
- package/server/lib/realtime-proxy-terminal.test.js +150 -0
- package/server/lib/realtime-proxy.js +2 -0
- package/server/lib/routing/DOCUMENTATION.md +83 -0
- package/server/lib/routing/defaults.js +98 -0
- package/server/lib/routing/feature-flag.js +11 -0
- package/server/lib/routing/history.js +34 -0
- package/server/lib/routing/jev.js +98 -0
- package/server/lib/routing/routes.http.test.js +100 -0
- package/server/lib/routing/routes.js +81 -0
- package/server/lib/routing/runtime.js +217 -0
- package/server/lib/routing/runtime.test.js +188 -0
- package/server/lib/routing/store.js +249 -0
- package/server/lib/routing/store.test.js +96 -0
- package/server/lib/scheduled-tasks/runtime.js +2 -2
- package/server/lib/scheduled-tasks/runtime.test.js +14 -0
- package/server/lib/security/request-security.js +9 -5
- package/server/lib/security/request-security.test.js +37 -0
- package/server/lib/small-model/DOCUMENTATION.md +14 -2
- package/server/lib/small-model/call.js +37 -6
- package/server/lib/small-model/call.test.js +182 -0
- package/server/lib/terminal/DOCUMENTATION.md +23 -2
- package/server/lib/terminal/runtime.js +27 -5
- package/server/lib/terminal/runtime.test.js +67 -0
- package/server/lib/terminal/shutdown.js +106 -0
- package/server/lib/terminal/shutdown.test.js +84 -0
- package/server/lib/tts/service.test.js +78 -0
- package/server/lib/ui-auth/DOCUMENTATION.md +8 -0
- package/server/lib/ui-auth/session-cookie.js +51 -0
- package/server/lib/ui-auth/session-cookie.test.js +46 -0
- package/server/lib/ui-auth/ui-auth.js +56 -17
- package/server/lib/ui-auth/ui-auth.test.js +96 -0
- package/dist/assets/DiffView-CyLmb-wF.js +0 -4
- package/dist/assets/DirtyBranchSwitchDialog-pu5kcoir.js +0 -1
- package/dist/assets/FilesView-WtJB0y_l.js +0 -13
- package/dist/assets/GitView-DkNrV0BK.js +0 -6
- package/dist/assets/LinearIssuesView-B0H0Otii.js +0 -1
- package/dist/assets/MarkdownRendererImpl-DSqVN2d8.js +0 -12
- package/dist/assets/PierreDiffViewer-Bsq9z-sw.js +0 -161
- package/dist/assets/PlanView-CAFziKH5.js +0 -5
- package/dist/assets/SettingsView-BRittavZ.js +0 -28
- package/dist/assets/ToolOutputDialog-B0uYaJio.js +0 -15
- package/dist/assets/appThemeRegistry-BK0dHKa6.js +0 -1
- package/dist/assets/de-c7CYLCAW.js +0 -10
- package/dist/assets/es-BKAVPQmQ.js +0 -16
- package/dist/assets/fr-Bj1glqfV.js +0 -14
- package/dist/assets/index-BYFyOu_D.css +0 -1
- package/dist/assets/index-BfwRClDe.js +0 -40
- package/dist/assets/ja-5jwSIbK0.js +0 -16
- package/dist/assets/ko-BaDbg-Eb.js +0 -16
- package/dist/assets/main-CGG2_uHq.js +0 -2
- package/dist/assets/main-CcrSaL4J.js +0 -930
- package/dist/assets/markdownCore-DsmQr3bt.js +0 -5
- package/dist/assets/miniChat-DEaAhP4b.js +0 -2
- package/dist/assets/mobile-BC3rsvv0.js +0 -2
- package/dist/assets/pl-BFKVBDFu.js +0 -16
- package/dist/assets/pt-BR-qKfHvkDL.js +0 -16
- package/dist/assets/registerCustomTheme-BBXUCRnG.js +0 -1
- package/dist/assets/renderElectronMiniChatApp-CR9d1Mr0.js +0 -2
- package/dist/assets/renderMobileApp-Da5dwobZ.js +0 -25
- package/dist/assets/runtimeEndpointReset-CLYVRw2H.js +0 -41
- package/dist/assets/shikiHighlight-D1Z9G58W.js +0 -1
- package/dist/assets/tr-DYXPoEDG.js +0 -7
- package/dist/assets/uk-CwAoloR8.js +0 -16
- package/dist/assets/useAppFontEffects-DeStGcUT.js +0 -7282
- package/dist/assets/useGitComparison-CpxioduU.js +0 -1
- package/dist/assets/useInlineCommentController-gmUnJtrb.js +0 -2
- package/dist/assets/useRootScrollLock--a3cPz8s.js +0 -1
- package/dist/assets/vendor-framer-motion-livSKyyV.js +0 -1
- package/dist/assets/zh-CN-D9MG0lqF.js +0 -16
- package/dist/assets/zh-TW-1HNK7shh.js +0 -16
- package/server/lib/quota/providers/crof.js +0 -96
- package/server/lib/quota/providers/crof.test.js +0 -98
- /package/dist/assets/{vendor-katex-k3Ppq1Ha.js → vendor-katex-CHD9BdDO.js} +0 -0
|
@@ -0,0 +1,838 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { guestStorageRequestSchema } from '@openchamber/sdk/schemas';
|
|
3
|
+
import { runGuestStorage } from './storage.js';
|
|
4
|
+
import fs from 'node:fs/promises';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
GUEST_CAPABILITIES,
|
|
10
|
+
GUEST_GENERATE_OUTPUT_TOKENS_MAX,
|
|
11
|
+
GUEST_GENERATE_PROMPT_MAX,
|
|
12
|
+
GUEST_GENERATE_SYSTEM_MAX,
|
|
13
|
+
GUEST_GENERATE_TEXT_MAX,
|
|
14
|
+
GUEST_FILE_CONTENT_MAX,
|
|
15
|
+
GUEST_FILE_PATH_MAX,
|
|
16
|
+
isGuestRequestPath,
|
|
17
|
+
requestedGuestCapabilities,
|
|
18
|
+
resolveIntegrationAuth,
|
|
19
|
+
} from '@openchamber/sdk';
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
findInstalledGuest,
|
|
23
|
+
isGuestPanelId,
|
|
24
|
+
listInstalledGuests,
|
|
25
|
+
resolveGuestServedFile,
|
|
26
|
+
toPublicGuest,
|
|
27
|
+
} from './catalog.js';
|
|
28
|
+
import { runGuestFileOperation } from './files.js';
|
|
29
|
+
import { injectGuestAssetTokens, parseGuestUrlToken } from './html-tokens.js';
|
|
30
|
+
import { injectGuestDocumentStyles } from './html-styles.js';
|
|
31
|
+
import { installGuest, installGuestFromZipBuffer, parseInstallRequest, uninstallGuest } from './install.js';
|
|
32
|
+
import { guestUploadMaxBytes, readGuestUploadBody } from './upload.js';
|
|
33
|
+
import { checkAllGuestUpdates, updateGuest, withGuestUpdate } from './updates.js';
|
|
34
|
+
import { extensionsPersistPath, readExtensionStore, setCapabilityGrants } from './persist.js';
|
|
35
|
+
import { guestGrantScope, sameCredentialTarget } from './grant-scope.js';
|
|
36
|
+
import { dropGuestTokens, forgetGuestAuth, getGuestAuth, guestAuthPersistPath, patchGuestAuth } from './auth-store.js';
|
|
37
|
+
import {
|
|
38
|
+
disconnectHostGuest,
|
|
39
|
+
startHostGuestAuthorization,
|
|
40
|
+
toGuestAuthResponse,
|
|
41
|
+
} from './host-session.js';
|
|
42
|
+
import {
|
|
43
|
+
GuestOAuthError,
|
|
44
|
+
consumeGuestAuthorization,
|
|
45
|
+
disconnectGuestAuth,
|
|
46
|
+
saveGuestOAuthClient,
|
|
47
|
+
guestRedirectUri,
|
|
48
|
+
saveGuestAccessToken,
|
|
49
|
+
startGuestAuthorization,
|
|
50
|
+
takeUsableGuestAuth,
|
|
51
|
+
toPublicGuestAuth,
|
|
52
|
+
} from './oauth.js';
|
|
53
|
+
import { proxyGuestRequest } from './request.js';
|
|
54
|
+
import {
|
|
55
|
+
GuestServiceError,
|
|
56
|
+
getServiceStatus,
|
|
57
|
+
proxyGuestServiceRequest,
|
|
58
|
+
setServiceSocketOverride,
|
|
59
|
+
setGuestEnabled,
|
|
60
|
+
stopGuestService,
|
|
61
|
+
} from './service.js';
|
|
62
|
+
|
|
63
|
+
const json16 = express.json({ limit: '16kb' });
|
|
64
|
+
const json80 = express.json({ limit: '80kb' });
|
|
65
|
+
// GUEST_FILE_CONTENT_MAX characters can be several bytes each once JSON-escaped.
|
|
66
|
+
const jsonFiles = express.json({ limit: '12mb' });
|
|
67
|
+
// A 64k-character prompt in a multi-byte script is a few hundred KB of JSON.
|
|
68
|
+
const jsonGenerate = express.json({ limit: '512kb' });
|
|
69
|
+
|
|
70
|
+
const clientBodySchema = z.object({
|
|
71
|
+
clientId: z.string().trim().min(1).max(400),
|
|
72
|
+
clientSecret: z.string().trim().min(1).max(400).optional(),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const tokenBodySchema = z.object({
|
|
76
|
+
token: z.string().trim().min(1).max(800),
|
|
77
|
+
username: z.string().trim().max(200).optional(),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const capabilityGrantSchema = z.object({
|
|
81
|
+
granted: z.array(z.enum(GUEST_CAPABILITIES)).max(GUEST_CAPABILITIES.length),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const requestBodySchema = z.object({
|
|
85
|
+
method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']),
|
|
86
|
+
path: z.string().trim().min(1).refine(isGuestRequestPath),
|
|
87
|
+
query: z.record(z.string().min(1).max(128), z.string().max(2000)).optional(),
|
|
88
|
+
body: z.string().max(64_000).optional(),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const fileBodySchema = z.object({
|
|
92
|
+
op: z.enum(['read', 'write', 'list', 'stat']),
|
|
93
|
+
path: z.string().min(1).max(GUEST_FILE_PATH_MAX),
|
|
94
|
+
content: z.string().max(GUEST_FILE_CONTENT_MAX).optional(),
|
|
95
|
+
}).refine((value) => value.op !== 'write' || typeof value.content === 'string', { path: ['content'] });
|
|
96
|
+
|
|
97
|
+
const generateBodySchema = z.object({
|
|
98
|
+
prompt: z.string().trim().min(1).max(GUEST_GENERATE_PROMPT_MAX),
|
|
99
|
+
system: z.string().trim().min(1).max(GUEST_GENERATE_SYSTEM_MAX).optional(),
|
|
100
|
+
maxOutputTokens: z.number().int().min(1).max(GUEST_GENERATE_OUTPUT_TOKENS_MAX).optional(),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const socketOverrideBodySchema = z.object({
|
|
104
|
+
id: z.string().trim().regex(/^[a-z][a-z0-9-]*$/).max(64),
|
|
105
|
+
path: z.union([
|
|
106
|
+
z.string().trim().min(1).max(512).refine((value) => !value.includes('\0')),
|
|
107
|
+
z.literal(''),
|
|
108
|
+
z.null(),
|
|
109
|
+
]).optional(),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const enabledBodySchema = z.object({
|
|
113
|
+
enabled: z.boolean(),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const updateCheckBodySchema = z.object({
|
|
117
|
+
force: z.boolean().optional(),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const queryValue = (req, key) => {
|
|
121
|
+
const raw = req.query?.[key];
|
|
122
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
123
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const requestOrigin = (req) => {
|
|
127
|
+
const forwarded = req.headers['x-forwarded-proto'];
|
|
128
|
+
const proto = typeof forwarded === 'string' && forwarded.split(',')[0]
|
|
129
|
+
? forwarded.split(',')[0].trim()
|
|
130
|
+
: req.secure
|
|
131
|
+
? 'https'
|
|
132
|
+
: 'http';
|
|
133
|
+
const hostHeader = req.headers['x-forwarded-host'] || req.headers.host;
|
|
134
|
+
const host = typeof hostHeader === 'string' ? hostHeader.split(',')[0].trim() : '';
|
|
135
|
+
if (!host) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
return `${proto}://${host}`;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const escapeHtml = (value) => String(value)
|
|
142
|
+
.replace(/&/g, '&')
|
|
143
|
+
.replace(/</g, '<')
|
|
144
|
+
.replace(/>/g, '>')
|
|
145
|
+
.replace(/"/g, '"')
|
|
146
|
+
.replace(/'/g, ''');
|
|
147
|
+
|
|
148
|
+
const renderOauthCallbackPage = ({ title, message }) => `<!doctype html>
|
|
149
|
+
<html lang="en">
|
|
150
|
+
<head>
|
|
151
|
+
<meta charset="utf-8">
|
|
152
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
153
|
+
<title>${escapeHtml(title)} — OpenChamber</title>
|
|
154
|
+
<style>
|
|
155
|
+
:root { color-scheme: light dark; }
|
|
156
|
+
body { margin: 0; min-height: 100vh; display: flex; align-items: center; justify-content: center;
|
|
157
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
158
|
+
background: Canvas; color: CanvasText; }
|
|
159
|
+
main { max-width: 34rem; padding: 2.5rem 2rem; text-align: center; }
|
|
160
|
+
h1 { font-size: 1.25rem; margin: 0 0 0.75rem; }
|
|
161
|
+
p { margin: 0; line-height: 1.5; opacity: 0.85; }
|
|
162
|
+
</style>
|
|
163
|
+
</head>
|
|
164
|
+
<body>
|
|
165
|
+
<main>
|
|
166
|
+
<h1>${escapeHtml(title)}</h1>
|
|
167
|
+
<p>${escapeHtml(message)}</p>
|
|
168
|
+
</main>
|
|
169
|
+
</body>
|
|
170
|
+
</html>`;
|
|
171
|
+
|
|
172
|
+
const sendInstallResult = (res, result) => {
|
|
173
|
+
if (!result.ok) {
|
|
174
|
+
const conflict = result.code === 'id-taken' || result.code === 'already-installed';
|
|
175
|
+
const body = { error: result.code };
|
|
176
|
+
if (result.code === 'host-too-old' && result.required) {
|
|
177
|
+
body.required = result.required;
|
|
178
|
+
}
|
|
179
|
+
if (conflict && result.id) {
|
|
180
|
+
body.id = result.id;
|
|
181
|
+
}
|
|
182
|
+
return res.status(conflict ? 409 : 400).json(body);
|
|
183
|
+
}
|
|
184
|
+
res.status(result.replaced ? 200 : 201).json({ guest: result.guest });
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const declaredSettings = (guest) => {
|
|
188
|
+
const fields = guest.integration?.settings ?? [];
|
|
189
|
+
return new Map(fields.map((field) => [field.id, field]));
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
export const registerGuestRoutes = (app, {
|
|
193
|
+
openchamberDataDir,
|
|
194
|
+
openchamberVersion,
|
|
195
|
+
resolveGitBinaryForSpawn,
|
|
196
|
+
resolveOptionalProjectDirectory,
|
|
197
|
+
getSmallModelService,
|
|
198
|
+
}) => {
|
|
199
|
+
const persistPath = extensionsPersistPath(openchamberDataDir);
|
|
200
|
+
const authPath = guestAuthPersistPath(openchamberDataDir);
|
|
201
|
+
const versionOptions = { openchamberVersion };
|
|
202
|
+
const installOptions = () => ({ openchamberVersion, gitBinary: resolveGitBinaryForSpawn() });
|
|
203
|
+
|
|
204
|
+
const loadGuest = async (id) => {
|
|
205
|
+
if (!isGuestPanelId(id)) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
return findInstalledGuest(id, persistPath);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
app.get('/api/guests', async (_req, res) => {
|
|
212
|
+
try {
|
|
213
|
+
const guests = await listInstalledGuests({ persistPath });
|
|
214
|
+
res.json({
|
|
215
|
+
guests: guests.map((guest) => toPublicGuest(withGuestUpdate(guest, persistPath))),
|
|
216
|
+
});
|
|
217
|
+
} catch (error) {
|
|
218
|
+
console.error('Failed to list guests:', error);
|
|
219
|
+
res.status(500).json({ error: 'Failed to list guests' });
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
app.post('/api/guests', json16, async (req, res) => {
|
|
224
|
+
try {
|
|
225
|
+
const request = parseInstallRequest(req.body);
|
|
226
|
+
if (!request) {
|
|
227
|
+
const hasUrl = typeof req.body?.url === 'string';
|
|
228
|
+
return res.status(400).json({ error: hasUrl ? 'invalid-url' : 'invalid-path' });
|
|
229
|
+
}
|
|
230
|
+
const result = await installGuest(request, persistPath, installOptions());
|
|
231
|
+
sendInstallResult(res, result);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
console.error('Failed to install guest:', error);
|
|
234
|
+
res.status(500).json({ error: 'Failed to install guest' });
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Raw zip body from the browser (Settings → Extensions file picker or
|
|
239
|
+
// drop) for hosts the user cannot name a path on. Registered ahead of the
|
|
240
|
+
// `:id` routes because `upload` is a valid panel id shape. Same result
|
|
241
|
+
// and error codes as `POST /api/guests` with a local `.zip`.
|
|
242
|
+
app.post('/api/guests/upload', async (req, res) => {
|
|
243
|
+
try {
|
|
244
|
+
const body = await readGuestUploadBody(req, guestUploadMaxBytes());
|
|
245
|
+
if (!body.ok) {
|
|
246
|
+
return res.status(body.status).json({ error: body.error });
|
|
247
|
+
}
|
|
248
|
+
const replace = req.query?.replace === 'true';
|
|
249
|
+
const result = await installGuestFromZipBuffer(body.buffer, persistPath, { openchamberVersion, replace });
|
|
250
|
+
sendInstallResult(res, result);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
console.error('Failed to install uploaded guest:', error);
|
|
253
|
+
res.status(500).json({ error: 'Failed to install guest' });
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Registered ahead of the `:id` routes so `updates` is never read as a guest id.
|
|
258
|
+
app.post('/api/guests/updates/check', json16, async (req, res) => {
|
|
259
|
+
try {
|
|
260
|
+
const parsed = updateCheckBodySchema.safeParse(req.body ?? {});
|
|
261
|
+
if (!parsed.success) {
|
|
262
|
+
return res.status(400).json({ error: 'invalid-request' });
|
|
263
|
+
}
|
|
264
|
+
const updates = await checkAllGuestUpdates({
|
|
265
|
+
persistPath,
|
|
266
|
+
force: Boolean(parsed.data.force),
|
|
267
|
+
gitBinary: resolveGitBinaryForSpawn(),
|
|
268
|
+
});
|
|
269
|
+
res.json({ updates });
|
|
270
|
+
} catch (error) {
|
|
271
|
+
console.error('Failed to check guest updates:', error);
|
|
272
|
+
res.status(500).json({ error: 'Failed to check guest updates' });
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
app.post('/api/guests/:id/update', async (req, res) => {
|
|
277
|
+
try {
|
|
278
|
+
const guest = await loadGuest(req.params.id);
|
|
279
|
+
if (!guest) {
|
|
280
|
+
return res.status(404).json({ error: 'not-found' });
|
|
281
|
+
}
|
|
282
|
+
const result = await updateGuest({
|
|
283
|
+
guest,
|
|
284
|
+
origin: guest.gitOrigin,
|
|
285
|
+
persistPath,
|
|
286
|
+
...installOptions(),
|
|
287
|
+
});
|
|
288
|
+
if (!result.ok) {
|
|
289
|
+
const body = { error: result.code };
|
|
290
|
+
if (result.code === 'host-too-old' && result.required) {
|
|
291
|
+
body.required = result.required;
|
|
292
|
+
}
|
|
293
|
+
return res.status(400).json(body);
|
|
294
|
+
}
|
|
295
|
+
const next = await loadGuest(guest.id);
|
|
296
|
+
if (!next) {
|
|
297
|
+
return res.status(404).json({ error: 'not-found' });
|
|
298
|
+
}
|
|
299
|
+
res.json({ guest: toPublicGuest(withGuestUpdate(next, persistPath)) });
|
|
300
|
+
} catch (error) {
|
|
301
|
+
console.error('Failed to update guest:', error);
|
|
302
|
+
res.status(500).json({ error: 'Failed to update guest' });
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
app.delete('/api/guests/:id', async (req, res) => {
|
|
307
|
+
try {
|
|
308
|
+
const id = req.params.id;
|
|
309
|
+
if (!isGuestPanelId(id)) {
|
|
310
|
+
return res.status(404).json({ error: 'not-found' });
|
|
311
|
+
}
|
|
312
|
+
const result = await uninstallGuest(id, persistPath);
|
|
313
|
+
if (!result.ok) {
|
|
314
|
+
const status = result.code === 'bundled' ? 400 : 404;
|
|
315
|
+
return res.status(status).json({ error: result.code });
|
|
316
|
+
}
|
|
317
|
+
// Remove means forget: tokens, client secret, and settings go with the package.
|
|
318
|
+
await forgetGuestAuth(id, authPath);
|
|
319
|
+
res.status(204).end();
|
|
320
|
+
} catch (error) {
|
|
321
|
+
console.error('Failed to uninstall guest:', error);
|
|
322
|
+
res.status(500).json({ error: 'Failed to uninstall guest' });
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
app.get('/api/guests/:id/oauth/status', async (req, res) => {
|
|
327
|
+
try {
|
|
328
|
+
const guest = await loadGuest(req.params.id);
|
|
329
|
+
if (!guest) {
|
|
330
|
+
return res.status(404).json({ error: 'not-found' });
|
|
331
|
+
}
|
|
332
|
+
const origin = requestOrigin(req);
|
|
333
|
+
// Tokens minted for addresses the package no longer names are dropped
|
|
334
|
+
// here, so the card shows disconnected instead of a stale account.
|
|
335
|
+
const stored = await takeUsableGuestAuth(guest, authPath);
|
|
336
|
+
const auth = resolveIntegrationAuth(guest.integration ?? {});
|
|
337
|
+
res.json({
|
|
338
|
+
...await toGuestAuthResponse(guest.integration, stored),
|
|
339
|
+
redirectUri: auth === 'oauth' && origin ? guestRedirectUri(origin, guest.id) : '',
|
|
340
|
+
});
|
|
341
|
+
} catch (error) {
|
|
342
|
+
console.error('Failed to read guest oauth status:', error);
|
|
343
|
+
res.status(500).json({ error: 'Failed to read guest oauth status' });
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
app.put('/api/guests/:id/oauth/client', json16, async (req, res) => {
|
|
348
|
+
try {
|
|
349
|
+
const guest = await loadGuest(req.params.id);
|
|
350
|
+
if (!guest?.integration) {
|
|
351
|
+
return res.status(404).json({ error: 'not-found' });
|
|
352
|
+
}
|
|
353
|
+
if (resolveIntegrationAuth(guest.integration) !== 'oauth') {
|
|
354
|
+
return res.status(400).json({ error: 'NO_INTEGRATION' });
|
|
355
|
+
}
|
|
356
|
+
const parsed = clientBodySchema.safeParse(req.body);
|
|
357
|
+
if (!parsed.success) {
|
|
358
|
+
return res.status(400).json({ error: 'invalid-client' });
|
|
359
|
+
}
|
|
360
|
+
const stored = await saveGuestOAuthClient({
|
|
361
|
+
guest,
|
|
362
|
+
persistPath: authPath,
|
|
363
|
+
clientId: parsed.data.clientId,
|
|
364
|
+
clientSecret: parsed.data.clientSecret,
|
|
365
|
+
});
|
|
366
|
+
res.json(toPublicGuestAuth(stored, guest.integration));
|
|
367
|
+
} catch (error) {
|
|
368
|
+
console.error('Failed to save guest oauth client:', error);
|
|
369
|
+
res.status(500).json({ error: 'Failed to save guest oauth client' });
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
app.put('/api/guests/:id/token', json16, async (req, res) => {
|
|
374
|
+
try {
|
|
375
|
+
const guest = await loadGuest(req.params.id);
|
|
376
|
+
if (!guest?.integration) {
|
|
377
|
+
return res.status(404).json({ error: 'not-found' });
|
|
378
|
+
}
|
|
379
|
+
if (resolveIntegrationAuth(guest.integration) !== 'token') {
|
|
380
|
+
return res.status(400).json({ error: 'NO_TOKEN_AUTH' });
|
|
381
|
+
}
|
|
382
|
+
const parsed = tokenBodySchema.safeParse(req.body);
|
|
383
|
+
if (!parsed.success) {
|
|
384
|
+
return res.status(400).json({ error: 'invalid-token' });
|
|
385
|
+
}
|
|
386
|
+
await saveGuestAccessToken({ guest, persistPath: authPath, token: parsed.data.token, username: parsed.data.username });
|
|
387
|
+
const stored = await getGuestAuth(guest.id, authPath);
|
|
388
|
+
res.json(await toGuestAuthResponse(guest.integration, stored));
|
|
389
|
+
} catch (error) {
|
|
390
|
+
if (error instanceof GuestOAuthError) {
|
|
391
|
+
const status = 400;
|
|
392
|
+
return res.status(status).json({ error: error.code, message: error.message });
|
|
393
|
+
}
|
|
394
|
+
console.error('Failed to save guest token:', error);
|
|
395
|
+
res.status(500).json({ error: 'Failed to save guest token' });
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
app.put('/api/guests/:id/settings', json16, async (req, res) => {
|
|
400
|
+
try {
|
|
401
|
+
const guest = await loadGuest(req.params.id);
|
|
402
|
+
if (!guest?.integration) {
|
|
403
|
+
return res.status(404).json({ error: 'not-found' });
|
|
404
|
+
}
|
|
405
|
+
if (!req.body || typeof req.body !== 'object' || Array.isArray(req.body)) {
|
|
406
|
+
return res.status(400).json({ error: 'invalid-settings' });
|
|
407
|
+
}
|
|
408
|
+
const allowed = declaredSettings(guest);
|
|
409
|
+
const settings = {};
|
|
410
|
+
for (const [key, value] of Object.entries(req.body)) {
|
|
411
|
+
if (!allowed.has(key) || typeof value !== 'string') {
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
settings[key] = value.trim().slice(0, 2000);
|
|
415
|
+
}
|
|
416
|
+
const stored = await patchGuestAuth(guest.id, { settings }, authPath);
|
|
417
|
+
res.json(await toGuestAuthResponse(guest.integration, stored));
|
|
418
|
+
} catch (error) {
|
|
419
|
+
console.error('Failed to save guest settings:', error);
|
|
420
|
+
res.status(500).json({ error: 'Failed to save guest settings' });
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
app.post('/api/guests/:id/oauth/start', json16, async (req, res) => {
|
|
425
|
+
try {
|
|
426
|
+
const guest = await loadGuest(req.params.id);
|
|
427
|
+
if (!guest?.integration) {
|
|
428
|
+
return res.status(404).json({ error: 'not-found' });
|
|
429
|
+
}
|
|
430
|
+
const auth = resolveIntegrationAuth(guest.integration);
|
|
431
|
+
if (auth === 'host') {
|
|
432
|
+
const started = await startHostGuestAuthorization(guest.integration);
|
|
433
|
+
if (!started) {
|
|
434
|
+
return res.status(400).json({ error: 'NO_INTEGRATION' });
|
|
435
|
+
}
|
|
436
|
+
return res.json(started);
|
|
437
|
+
}
|
|
438
|
+
const origin = requestOrigin(req);
|
|
439
|
+
if (!origin) {
|
|
440
|
+
return res.status(400).json({ error: 'missing-origin' });
|
|
441
|
+
}
|
|
442
|
+
const started = await startGuestAuthorization({ guest, persistPath: authPath, origin });
|
|
443
|
+
res.json(started);
|
|
444
|
+
} catch (error) {
|
|
445
|
+
if (error instanceof GuestOAuthError) {
|
|
446
|
+
const status = 400;
|
|
447
|
+
return res.status(status).json({ error: error.code, message: error.message });
|
|
448
|
+
}
|
|
449
|
+
console.error('Failed to start guest oauth:', error);
|
|
450
|
+
res.status(500).json({ error: 'Failed to start guest oauth' });
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
app.get('/api/guests/:id/oauth/callback', async (req, res) => {
|
|
455
|
+
const finish = (status, title, message) => {
|
|
456
|
+
res.status(status).type('html').send(renderOauthCallbackPage({ title, message }));
|
|
457
|
+
};
|
|
458
|
+
try {
|
|
459
|
+
const guest = await loadGuest(req.params.id);
|
|
460
|
+
if (!guest?.integration) {
|
|
461
|
+
return finish(404, 'Unknown extension', 'That extension is not installed.');
|
|
462
|
+
}
|
|
463
|
+
await consumeGuestAuthorization({
|
|
464
|
+
guest,
|
|
465
|
+
persistPath: authPath,
|
|
466
|
+
code: queryValue(req, 'code'),
|
|
467
|
+
state: queryValue(req, 'state'),
|
|
468
|
+
error: queryValue(req, 'error'),
|
|
469
|
+
errorDescription: queryValue(req, 'error_description'),
|
|
470
|
+
});
|
|
471
|
+
finish(200, 'Connected', 'You can close this tab and return to OpenChamber.');
|
|
472
|
+
} catch (error) {
|
|
473
|
+
if (error instanceof GuestOAuthError) {
|
|
474
|
+
return finish(400, 'Could not connect', error.message);
|
|
475
|
+
}
|
|
476
|
+
console.error('Failed to finish guest oauth:', error);
|
|
477
|
+
finish(500, 'Could not connect', 'The authorization callback failed.');
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
app.delete('/api/guests/:id/oauth', async (req, res) => {
|
|
482
|
+
try {
|
|
483
|
+
const guest = await loadGuest(req.params.id);
|
|
484
|
+
if (!guest) {
|
|
485
|
+
return res.status(404).json({ error: 'not-found' });
|
|
486
|
+
}
|
|
487
|
+
await disconnectHostGuest(guest.integration);
|
|
488
|
+
await disconnectGuestAuth(guest.id, authPath);
|
|
489
|
+
const stored = await getGuestAuth(guest.id, authPath);
|
|
490
|
+
res.json(await toGuestAuthResponse(guest.integration, stored));
|
|
491
|
+
} catch (error) {
|
|
492
|
+
console.error('Failed to disconnect guest oauth:', error);
|
|
493
|
+
res.status(500).json({ error: 'Failed to disconnect guest oauth' });
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
app.post('/api/guests/:id/request', json80, async (req, res) => {
|
|
498
|
+
try {
|
|
499
|
+
const guest = await loadGuest(req.params.id);
|
|
500
|
+
if (!guest?.integration) {
|
|
501
|
+
return res.status(404).json({ error: 'not-found' });
|
|
502
|
+
}
|
|
503
|
+
const store = await readExtensionStore(persistPath);
|
|
504
|
+
if (store.disabledGuests?.[guest.id]) {
|
|
505
|
+
throw new GuestOAuthError(
|
|
506
|
+
`${guest.name} is disabled in Settings → Extensions.`,
|
|
507
|
+
'DISABLED',
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
if (!guest.capabilityGrants.includes('network')) {
|
|
511
|
+
throw new GuestOAuthError(
|
|
512
|
+
`${guest.name} has not been allowed to use external services. Review it in Settings → Extensions.`,
|
|
513
|
+
'NOT_GRANTED',
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
const parsed = requestBodySchema.safeParse(req.body);
|
|
517
|
+
if (!parsed.success) {
|
|
518
|
+
return res.status(400).json({ error: 'invalid-request' });
|
|
519
|
+
}
|
|
520
|
+
const result = await proxyGuestRequest({
|
|
521
|
+
guest,
|
|
522
|
+
persistPath: authPath,
|
|
523
|
+
method: parsed.data.method,
|
|
524
|
+
path: parsed.data.path,
|
|
525
|
+
query: parsed.data.query,
|
|
526
|
+
body: parsed.data.body,
|
|
527
|
+
});
|
|
528
|
+
res.json(result);
|
|
529
|
+
} catch (error) {
|
|
530
|
+
if (error instanceof GuestOAuthError) {
|
|
531
|
+
const status = error.code === 'DISCONNECTED' ? 409 : 400;
|
|
532
|
+
return res.status(status).json({ error: error.code, message: error.message });
|
|
533
|
+
}
|
|
534
|
+
console.error('Failed to proxy guest request:', error);
|
|
535
|
+
res.status(500).json({ error: 'Failed to proxy guest request' });
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
app.post('/api/guests/:id/service/request', json80, async (req, res) => {
|
|
540
|
+
try {
|
|
541
|
+
const guest = await loadGuest(req.params.id);
|
|
542
|
+
if (!guest?.service) {
|
|
543
|
+
return res.status(404).json({ error: 'not-found' });
|
|
544
|
+
}
|
|
545
|
+
const parsed = requestBodySchema.safeParse(req.body);
|
|
546
|
+
if (!parsed.success) {
|
|
547
|
+
return res.status(400).json({ error: 'invalid-request' });
|
|
548
|
+
}
|
|
549
|
+
const result = await proxyGuestServiceRequest({
|
|
550
|
+
guestId: guest.id,
|
|
551
|
+
guestName: guest.name,
|
|
552
|
+
packageRoot: guest.packageRoot,
|
|
553
|
+
service: guest.service,
|
|
554
|
+
granted: guest.capabilityGrants,
|
|
555
|
+
persistPath,
|
|
556
|
+
method: parsed.data.method,
|
|
557
|
+
path: parsed.data.path,
|
|
558
|
+
query: parsed.data.query,
|
|
559
|
+
body: parsed.data.body,
|
|
560
|
+
});
|
|
561
|
+
res.json(result);
|
|
562
|
+
} catch (error) {
|
|
563
|
+
if (error instanceof GuestServiceError) {
|
|
564
|
+
const status = error.code === 'SERVICE_FAILED' ? 502 : 400;
|
|
565
|
+
return res.status(status).json({ error: error.code, message: error.message });
|
|
566
|
+
}
|
|
567
|
+
console.error('Failed to proxy guest service request:', error);
|
|
568
|
+
res.status(500).json({ error: 'Failed to proxy guest service request' });
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
app.post('/api/guests/:id/storage', json80, async (req, res) => {
|
|
573
|
+
const parsed = guestStorageRequestSchema.safeParse(req.body);
|
|
574
|
+
if (!parsed.success || !isGuestPanelId(req.params.id)) return res.status(400).json({ error: 'HOST_REJECTED', message: 'Invalid storage request.' });
|
|
575
|
+
try {
|
|
576
|
+
const result = await runGuestStorage(persistPath, req.params.id, parsed.data, async () => {
|
|
577
|
+
const guest = await loadGuest(req.params.id);
|
|
578
|
+
if (!guest || guest.enabled === false || (!guest.entry && !guest.backgroundEntry)) throw new Error('Extension is unavailable.');
|
|
579
|
+
if (!requestedGuestCapabilities(guest).every((capability) => guest.capabilityGrants.includes(capability))) throw new Error('Extension needs approval.');
|
|
580
|
+
});
|
|
581
|
+
return res.json(result);
|
|
582
|
+
} catch {
|
|
583
|
+
return res.status(400).json({ error: 'HOST_REJECTED', message: 'Storage operation failed. Check extension approval and storage limits.' });
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
app.post('/api/guests/:id/files', jsonFiles, async (req, res) => {
|
|
588
|
+
try {
|
|
589
|
+
const guest = await loadGuest(req.params.id);
|
|
590
|
+
if (!guest) {
|
|
591
|
+
return res.status(404).json({ error: 'not-found' });
|
|
592
|
+
}
|
|
593
|
+
const store = await readExtensionStore(persistPath);
|
|
594
|
+
if (store.disabledGuests?.[guest.id]) {
|
|
595
|
+
return res.status(400).json({ error: 'DISABLED', message: `${guest.name} is disabled in Settings → Extensions.` });
|
|
596
|
+
}
|
|
597
|
+
const parsed = fileBodySchema.safeParse(req.body);
|
|
598
|
+
if (!parsed.success) {
|
|
599
|
+
return res.status(400).json({ error: 'invalid-request' });
|
|
600
|
+
}
|
|
601
|
+
// No project header is a valid state (a filesystem-scope call); the
|
|
602
|
+
// runner answers NO_DIRECTORY for a relative path in that case.
|
|
603
|
+
const { directory } = await resolveOptionalProjectDirectory(req);
|
|
604
|
+
const result = await runGuestFileOperation({
|
|
605
|
+
op: parsed.data.op,
|
|
606
|
+
path: parsed.data.path,
|
|
607
|
+
content: parsed.data.content,
|
|
608
|
+
projectDirectory: directory,
|
|
609
|
+
patterns: guest.filesystem ?? [],
|
|
610
|
+
grants: guest.capabilityGrants,
|
|
611
|
+
homeDir: os.homedir(),
|
|
612
|
+
});
|
|
613
|
+
if (!result.ok) {
|
|
614
|
+
return res.status(result.code === 'NOT_FOUND' ? 404 : 400).json({ error: result.code, message: result.message });
|
|
615
|
+
}
|
|
616
|
+
res.json({ ok: true, result: result.result });
|
|
617
|
+
} catch (error) {
|
|
618
|
+
// Only the failure class is logged: never the path or the file content.
|
|
619
|
+
console.error('Failed to run guest file operation:', error?.code ?? error?.name ?? 'error');
|
|
620
|
+
res.status(500).json({ error: 'Failed to run guest file operation' });
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
// One-off text generation with the user's Small Model. The model is
|
|
625
|
+
// resolved and authenticated by the small-model service exactly as for the
|
|
626
|
+
// app's own background actions (session titles, notes); the guest never
|
|
627
|
+
// picks a provider and no session is involved. Prompt and answer are never
|
|
628
|
+
// logged.
|
|
629
|
+
app.post('/api/guests/:id/generate', jsonGenerate, async (req, res) => {
|
|
630
|
+
try {
|
|
631
|
+
const guest = await loadGuest(req.params.id);
|
|
632
|
+
if (!guest) {
|
|
633
|
+
return res.status(404).json({ error: 'not-found' });
|
|
634
|
+
}
|
|
635
|
+
const store = await readExtensionStore(persistPath);
|
|
636
|
+
if (store.disabledGuests?.[guest.id]) {
|
|
637
|
+
return res.status(400).json({ error: 'DISABLED', message: `${guest.name} is disabled in Settings → Extensions.` });
|
|
638
|
+
}
|
|
639
|
+
if (!guest.capabilityGrants.includes('model')) {
|
|
640
|
+
return res.status(400).json({
|
|
641
|
+
error: 'NOT_GRANTED',
|
|
642
|
+
message: `${guest.name} has not been allowed to use the Small Model. Review it in Settings → Extensions.`,
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
const parsed = generateBodySchema.safeParse(req.body);
|
|
646
|
+
if (!parsed.success) {
|
|
647
|
+
return res.status(400).json({ error: 'invalid-request' });
|
|
648
|
+
}
|
|
649
|
+
const { directory } = await resolveOptionalProjectDirectory(req);
|
|
650
|
+
const { generateSmallModelText } = await getSmallModelService();
|
|
651
|
+
let generated;
|
|
652
|
+
try {
|
|
653
|
+
generated = await generateSmallModelText({
|
|
654
|
+
prompt: parsed.data.prompt,
|
|
655
|
+
system: parsed.data.system,
|
|
656
|
+
maxOutputTokens: parsed.data.maxOutputTokens,
|
|
657
|
+
directory: directory || undefined,
|
|
658
|
+
});
|
|
659
|
+
} catch (error) {
|
|
660
|
+
const statusCode = Number(error?.statusCode) || 500;
|
|
661
|
+
if (statusCode === 404 || statusCode === 422) {
|
|
662
|
+
return res.status(400).json({
|
|
663
|
+
error: 'NO_MODEL',
|
|
664
|
+
message: 'No Small Model is available. Choose one in Settings → Sessions → Small Model.',
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
// The provider's error line names the model and status, never the prompt.
|
|
668
|
+
console.error('Guest small-model generation failed:', error?.message ?? error?.code ?? 'error');
|
|
669
|
+
return res.status(502).json({ error: 'MODEL_FAILED', message: 'The Small Model could not complete this request.' });
|
|
670
|
+
}
|
|
671
|
+
res.json({ ok: true, result: { text: String(generated?.text ?? '').slice(0, GUEST_GENERATE_TEXT_MAX) } });
|
|
672
|
+
} catch (error) {
|
|
673
|
+
console.error('Failed to run guest generation:', error?.code ?? error?.name ?? 'error');
|
|
674
|
+
res.status(500).json({ error: 'Failed to run guest generation' });
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
app.get('/api/guests/:id/service/status', async (req, res) => {
|
|
679
|
+
try {
|
|
680
|
+
const guest = await loadGuest(req.params.id);
|
|
681
|
+
if (!guest?.service) {
|
|
682
|
+
return res.status(404).json({ error: 'not-found' });
|
|
683
|
+
}
|
|
684
|
+
res.json({ status: getServiceStatus(guest.id) });
|
|
685
|
+
} catch (error) {
|
|
686
|
+
console.error('Failed to read guest service status:', error);
|
|
687
|
+
res.status(500).json({ error: 'Failed to read guest service status' });
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
app.put('/api/guests/:id/capabilities', json16, async (req, res) => {
|
|
692
|
+
try {
|
|
693
|
+
const guest = await loadGuest(req.params.id);
|
|
694
|
+
if (!guest) {
|
|
695
|
+
return res.status(404).json({ error: 'not-found' });
|
|
696
|
+
}
|
|
697
|
+
if (guest.source === 'bundled') {
|
|
698
|
+
return res.status(400).json({ error: 'bundled' });
|
|
699
|
+
}
|
|
700
|
+
const parsed = capabilityGrantSchema.safeParse(req.body);
|
|
701
|
+
if (!parsed.success) {
|
|
702
|
+
return res.status(400).json({ error: 'invalid-request' });
|
|
703
|
+
}
|
|
704
|
+
// Approval covers exactly what the installed package asks for. A grant
|
|
705
|
+
// for something the manifest does not request is meaningless and a
|
|
706
|
+
// partial grant would leave the guest half-working, so both are refused.
|
|
707
|
+
const requested = requestedGuestCapabilities(guest);
|
|
708
|
+
const granted = parsed.data.granted;
|
|
709
|
+
const matchesRequest = granted.length === requested.length && requested.every((capability) => granted.includes(capability));
|
|
710
|
+
if (granted.length > 0 && !matchesRequest) {
|
|
711
|
+
return res.status(400).json({ error: 'invalid-request' });
|
|
712
|
+
}
|
|
713
|
+
const scope = guestGrantScope(guest);
|
|
714
|
+
const store = await readExtensionStore(persistPath);
|
|
715
|
+
const previousScope = store.capabilityScopes?.[guest.id];
|
|
716
|
+
await setCapabilityGrants(guest.id, persistPath, granted, granted.length > 0 ? scope : null);
|
|
717
|
+
if (granted.length === 0) {
|
|
718
|
+
await stopGuestService(guest.id);
|
|
719
|
+
}
|
|
720
|
+
// Credentials were stored for one API origin and one pair of OAuth
|
|
721
|
+
// endpoints. When a newer version points the integration somewhere
|
|
722
|
+
// else, they must not follow it.
|
|
723
|
+
if (previousScope?.apiOrigin && scope.apiOrigin && !sameCredentialTarget(previousScope, scope)) {
|
|
724
|
+
await dropGuestTokens(guest.id, authPath);
|
|
725
|
+
}
|
|
726
|
+
const next = await loadGuest(guest.id);
|
|
727
|
+
if (!next) {
|
|
728
|
+
return res.status(404).json({ error: 'not-found' });
|
|
729
|
+
}
|
|
730
|
+
res.json({ guest: toPublicGuest(next) });
|
|
731
|
+
} catch (error) {
|
|
732
|
+
console.error('Failed to record guest capabilities:', error);
|
|
733
|
+
res.status(500).json({ error: 'Failed to record guest capabilities' });
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
app.put('/api/guests/:id/enabled', json16, async (req, res) => {
|
|
738
|
+
try {
|
|
739
|
+
const guest = await loadGuest(req.params.id);
|
|
740
|
+
if (!guest) {
|
|
741
|
+
return res.status(404).json({ error: 'not-found' });
|
|
742
|
+
}
|
|
743
|
+
const parsed = enabledBodySchema.safeParse(req.body);
|
|
744
|
+
if (!parsed.success) {
|
|
745
|
+
return res.status(400).json({ error: 'invalid-request' });
|
|
746
|
+
}
|
|
747
|
+
await setGuestEnabled(guest.id, persistPath, parsed.data.enabled);
|
|
748
|
+
const next = await loadGuest(guest.id);
|
|
749
|
+
if (!next) {
|
|
750
|
+
return res.status(404).json({ error: 'not-found' });
|
|
751
|
+
}
|
|
752
|
+
res.json({ guest: toPublicGuest(next) });
|
|
753
|
+
} catch (error) {
|
|
754
|
+
console.error('Failed to update guest enabled state:', error);
|
|
755
|
+
res.status(500).json({ error: 'Failed to update guest enabled state' });
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
app.put('/api/guests/:id/service/sockets', json16, async (req, res) => {
|
|
760
|
+
try {
|
|
761
|
+
const guest = await loadGuest(req.params.id);
|
|
762
|
+
if (!guest?.service) {
|
|
763
|
+
return res.status(404).json({ error: 'not-found' });
|
|
764
|
+
}
|
|
765
|
+
const parsed = socketOverrideBodySchema.safeParse(req.body);
|
|
766
|
+
if (!parsed.success) {
|
|
767
|
+
return res.status(400).json({ error: 'invalid-request' });
|
|
768
|
+
}
|
|
769
|
+
const declared = guest.service.permissions?.sockets ?? [];
|
|
770
|
+
if (!declared.some((binding) => binding.id === parsed.data.id)) {
|
|
771
|
+
return res.status(400).json({ error: 'unknown-socket', message: 'Socket id is not declared by this service.' });
|
|
772
|
+
}
|
|
773
|
+
await setServiceSocketOverride(
|
|
774
|
+
guest.id,
|
|
775
|
+
parsed.data.id,
|
|
776
|
+
persistPath,
|
|
777
|
+
parsed.data.path ?? null,
|
|
778
|
+
);
|
|
779
|
+
const next = await loadGuest(guest.id);
|
|
780
|
+
if (!next) {
|
|
781
|
+
return res.status(404).json({ error: 'not-found' });
|
|
782
|
+
}
|
|
783
|
+
res.json({ guest: toPublicGuest(next) });
|
|
784
|
+
} catch (error) {
|
|
785
|
+
console.error('Failed to update guest service socket path:', error);
|
|
786
|
+
res.status(500).json({ error: 'Failed to update guest service socket path' });
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
app.get('/api/guests/:id/{*filePath}', async (req, res) => {
|
|
791
|
+
try {
|
|
792
|
+
const id = req.params.id;
|
|
793
|
+
if (!isGuestPanelId(id)) {
|
|
794
|
+
return res.status(404).end();
|
|
795
|
+
}
|
|
796
|
+
const guest = await findInstalledGuest(id, persistPath);
|
|
797
|
+
if (!guest) {
|
|
798
|
+
return res.status(404).end();
|
|
799
|
+
}
|
|
800
|
+
const rawPath = req.params.filePath;
|
|
801
|
+
let relativePath;
|
|
802
|
+
try {
|
|
803
|
+
relativePath = decodeURIComponent(Array.isArray(rawPath) ? rawPath.join('/') : (rawPath || ''));
|
|
804
|
+
} catch {
|
|
805
|
+
return res.status(404).end();
|
|
806
|
+
}
|
|
807
|
+
const served = await resolveGuestServedFile(guest.packageRoot, relativePath, {
|
|
808
|
+
hasRuntime: Boolean(guest.entry || guest.backgroundEntry),
|
|
809
|
+
});
|
|
810
|
+
if (!served) {
|
|
811
|
+
return res.status(404).end();
|
|
812
|
+
}
|
|
813
|
+
const { filePath, contentType } = served;
|
|
814
|
+
let raw;
|
|
815
|
+
try {
|
|
816
|
+
raw = await fs.readFile(filePath);
|
|
817
|
+
} catch {
|
|
818
|
+
return res.status(404).end();
|
|
819
|
+
}
|
|
820
|
+
const token = parseGuestUrlToken(req.query.oc_url_token);
|
|
821
|
+
const body = contentType.startsWith('text/html')
|
|
822
|
+
? injectGuestDocumentStyles(injectGuestAssetTokens(raw.toString('utf8'), token))
|
|
823
|
+
: raw;
|
|
824
|
+
res.setHeader('Content-Type', contentType);
|
|
825
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
826
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
827
|
+
// Guest files are third-party code served from the OpenChamber origin.
|
|
828
|
+
// The rail embeds them in a sandboxed iframe; this header makes the
|
|
829
|
+
// document sandboxed even when opened directly, so a guest page can
|
|
830
|
+
// never run with the user's session on the app origin.
|
|
831
|
+
res.setHeader('Content-Security-Policy', 'sandbox allow-scripts');
|
|
832
|
+
res.send(body);
|
|
833
|
+
} catch (error) {
|
|
834
|
+
console.error('Failed to serve guest asset:', error);
|
|
835
|
+
res.status(500).json({ error: 'Failed to serve guest asset' });
|
|
836
|
+
}
|
|
837
|
+
});
|
|
838
|
+
};
|