@vantran-se/openchamber-web 1.23.0 → 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-DFg2wnpo.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-BKdsjg5B.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-_5gEVr0X.js → MarkdownImageGallery-BrFjAVEf.js} +1 -1
- package/dist/assets/MarkdownRendererImpl-DWepTafh.js +12 -0
- package/dist/assets/{OnboardingScreen-DdP4_nuc.js → OnboardingScreen-Dyxxiqdt.js} +1 -1
- package/dist/assets/{PatchDiff-am3SAss2.js → PatchDiff-DnEhjXcC.js} +1 -1
- package/dist/assets/PierreDiffViewer-DyxZVVVR.js +210 -0
- package/dist/assets/{PlanView-BoJxdRZe.js → PlanView-LW2z_EAP.js} +4 -4
- package/dist/assets/PluginPane-B4tKno2h.js +3 -0
- package/dist/assets/SettingsView-DEaMulN9.js +29 -0
- package/dist/assets/{SettingsWindow-DVTdDSol.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-C7WLk5i9.js → ToolPartDiffPreview-CkR4msKP.js} +2 -2
- package/dist/assets/{VirtualizedFileDiff-DcdgkH-x.js → VirtualizedFileDiff-0pyr_mHY.js} +4 -4
- package/dist/assets/WalkthroughView-yNj4nOP5.js +4 -0
- package/dist/assets/appThemeRegistry-BuUKhbYQ.js +1 -0
- package/dist/assets/commitAndPush-DvJKa3Ks.js +1 -0
- package/dist/assets/{computeVirtualFileMetrics-C8byMnRh.js → computeVirtualFileMetrics-DxTBMuu-.js} +2 -2
- 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-BB7ryz8y.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-45ujMPGb.js → isDiffPlainText-D7FkBMP6.js} +1 -1
- package/dist/assets/{isFilePlainText-Cc4KFSYi.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-Bsba8KFp.js +13 -0
- 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-DzjbLd8P.js +1 -0
- package/dist/assets/shikiHighlight-8_3csmr6.js +1 -0
- package/dist/assets/surface-B6n7wJtt.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-DdV8vhHn.js → vendor-base-ui-CsXYnR5c.js} +1 -1
- package/dist/assets/{vendor-codemirror-language-data-DzlSIu_W.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-CgAdXWZO.js +39 -0
- package/dist/assets/{web-components-CdGfR2XJ.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 +47 -23
- package/server/lib/agent-memory/runtime.js +7 -2
- package/server/lib/agent-memory/runtime.test.js +22 -0
- 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 +26 -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 +68 -13
- package/server/lib/event-stream/global-hub.test.js +182 -0
- package/server/lib/event-stream/global-ws-bridge.js +16 -18
- package/server/lib/event-stream/index.js +4 -0
- package/server/lib/event-stream/protocol.js +21 -7
- package/server/lib/event-stream/runtime.js +9 -6
- package/server/lib/event-stream/runtime.test.js +60 -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 +30 -6
- package/server/lib/git/routes.js +37 -11
- package/server/lib/git/routes.test.js +78 -0
- 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 +588 -74
- package/server/lib/git/service.test.js +492 -0
- package/server/lib/github/DOCUMENTATION.md +5 -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 +6 -6
- 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 +17 -1
- package/server/lib/message-queue/runtime.js +34 -1
- package/server/lib/message-queue/runtime.test.js +83 -6
- package/server/lib/notifications/DOCUMENTATION.md +1 -1
- package/server/lib/notifications/emitter-runtime.js +4 -3
- 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 +73 -5
- package/server/lib/opencode/bootstrap-runtime.js +1 -0
- package/server/lib/opencode/core-routes.js +39 -1
- package/server/lib/opencode/env-runtime.js +5 -1
- package/server/lib/opencode/env-runtime.test.js +72 -0
- package/server/lib/opencode/feature-routes-runtime.js +30 -1
- package/server/lib/opencode/lifecycle-process.test.js +84 -0
- package/server/lib/opencode/lifecycle.js +58 -70
- package/server/lib/opencode/lifecycle.test.js +29 -0
- package/server/lib/opencode/managed-plugin-config.js +1 -1
- package/server/lib/opencode/openchamber-routes.js +72 -14
- package/server/lib/opencode/openchamber-routes.test.js +71 -3
- package/server/lib/opencode/proxy.js +33 -0
- 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 +11 -0
- package/server/lib/opencode/settings-helpers.test.js +24 -2
- 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 +18 -0
- package/server/lib/opencode/settings-runtime.js +36 -9
- package/server/lib/opencode/settings-runtime.test.js +158 -2
- 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/project-context/DOCUMENTATION.md +13 -3
- package/server/lib/project-context/runtime.js +8 -2
- package/server/lib/project-context/runtime.test.js +54 -0
- package/server/lib/projects/DOCUMENTATION.md +2 -1
- package/server/lib/projects/project-config.js +40 -11
- package/server/lib/projects/project-config.test.js +97 -0
- package/server/lib/projects/project-id.js +26 -0
- 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/relay/DOCUMENTATION.md +20 -0
- package/server/lib/relay/host-client.js +7 -0
- package/server/lib/relay/host-client.test.js +14 -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/DOCUMENTATION.md +9 -1
- package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +28 -0
- package/server/lib/scheduled-tasks/runtime.js +14 -4
- package/server/lib/scheduled-tasks/runtime.test.js +67 -0
- package/server/lib/security/request-security.js +9 -5
- package/server/lib/security/request-security.test.js +37 -0
- package/server/lib/session-assist/DOCUMENTATION.md +131 -66
- package/server/lib/session-assist/context.js +127 -0
- package/server/lib/session-assist/context.test.js +132 -0
- package/server/lib/session-assist/prompt.js +54 -0
- package/server/lib/session-assist/runtime.js +111 -257
- package/server/lib/session-assist/runtime.test.js +203 -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/server/lib/walkthrough/DOCUMENTATION.md +15 -5
- package/server/lib/walkthrough/pr-source.test.js +21 -0
- package/server/lib/walkthrough/pull-request.js +7 -4
- package/server/lib/walkthrough/pull-request.test.js +16 -0
- package/server/lib/walkthrough/routes.js +19 -0
- package/server/lib/walkthrough/routes.test.js +26 -0
- package/server/lib/walkthrough/sources.js +13 -2
- package/server/opencode-worktree-readiness.test.js +130 -0
- package/dist/assets/DiffView-3Dn4QHbu.js +0 -4
- package/dist/assets/DirtyBranchSwitchDialog-CwOerqVi.js +0 -1
- package/dist/assets/FilesView-BdTOpew9.js +0 -17
- package/dist/assets/GitView-Dqf9vHJh.js +0 -6
- package/dist/assets/LinearIssuesView-CmbeL7I6.js +0 -1
- package/dist/assets/MarkdownRendererImpl-wVZlx4dd.js +0 -12
- package/dist/assets/PierreDiffViewer-DCsydlKs.js +0 -161
- package/dist/assets/SettingsView-CcM8pF5O.js +0 -28
- package/dist/assets/ToolOutputDialog-BgfTbDZ4.js +0 -15
- package/dist/assets/WalkthroughView-BEVUDxuw.js +0 -4
- package/dist/assets/appThemeRegistry-BK0dHKa6.js +0 -1
- package/dist/assets/de-C8jV7J9y.js +0 -10
- package/dist/assets/es-BI6J1M5R.js +0 -16
- package/dist/assets/fr-D6oDFt7E.js +0 -14
- package/dist/assets/index-BqhiCmYS.js +0 -39
- package/dist/assets/index-FqSOBz0J.css +0 -1
- package/dist/assets/ja-C_4i5EQu.js +0 -16
- package/dist/assets/ko-BFbOTpbq.js +0 -16
- package/dist/assets/main-C9ETLKFc.js +0 -2
- package/dist/assets/main-CJmb2dF8.js +0 -930
- package/dist/assets/markdownCore-DVuZpnli.js +0 -5
- package/dist/assets/miniChat-B4Ytoq2n.js +0 -2
- package/dist/assets/mobile-BUT6-lo1.js +0 -2
- package/dist/assets/patchFileDiff-8j1apICU.js +0 -13
- package/dist/assets/pl-COFvT56l.js +0 -16
- package/dist/assets/pt-BR-DtVOO2Lj.js +0 -16
- package/dist/assets/registerCustomTheme-BBXUCRnG.js +0 -1
- package/dist/assets/renderElectronMiniChatApp-B6vUG-vF.js +0 -2
- package/dist/assets/renderMobileApp-BNgHraq6.js +0 -25
- package/dist/assets/runtimeEndpointReset-B8LYpWvr.js +0 -17
- package/dist/assets/scheduledTasksApi-GC4epIYA.js +0 -1
- package/dist/assets/shikiHighlight-BJISeZsQ.js +0 -1
- package/dist/assets/surface-BvRzS58U.js +0 -1
- package/dist/assets/tr-B8Q0Go2l.js +0 -7
- package/dist/assets/uk-Dk4a-Fn-.js +0 -16
- package/dist/assets/useAppFontEffects-DpFNefUA.js +0 -7284
- package/dist/assets/useGitComparison-Dfxaqd0s.js +0 -1
- package/dist/assets/useInlineCommentController-CscsE4t-.js +0 -2
- package/dist/assets/useRootScrollLock-BaCvuRlT.js +0 -1
- package/dist/assets/vendor-framer-motion-livSKyyV.js +0 -1
- package/dist/assets/vendor-zod-BPAANqvH.js +0 -39
- package/dist/assets/zh-CN-B_BXxG1Q.js +0 -16
- package/dist/assets/zh-TW-DLrl1SPZ.js +0 -16
- package/server/lib/mcp-reconnect/DOCUMENTATION.md +0 -55
- package/server/lib/mcp-reconnect/runtime.js +0 -122
- package/server/lib/mcp-reconnect/runtime.test.js +0 -140
- 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,336 @@
|
|
|
1
|
+
import fsSync from 'node:fs';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
import { GUEST_CAPABILITIES } from '@openchamber/sdk';
|
|
7
|
+
|
|
8
|
+
const GUEST_SOURCES = ['path', 'zip', 'git'];
|
|
9
|
+
|
|
10
|
+
const storeSchema = z.object({
|
|
11
|
+
paths: z.array(z.string().min(1).refine((entry) => !entry.includes('\0'))),
|
|
12
|
+
sources: z.record(z.string(), z.enum(GUEST_SOURCES)).optional(),
|
|
13
|
+
// Where a git install came from, keyed by install path. Entries are checked
|
|
14
|
+
// one at a time on read so a malformed one drops out instead of hiding the
|
|
15
|
+
// whole catalog.
|
|
16
|
+
gitOrigins: z.record(z.string(), z.unknown()).optional(),
|
|
17
|
+
// Grants are stored as plain strings and filtered on read: a capability
|
|
18
|
+
// that this build no longer knows (renamed, removed) must not invalidate the
|
|
19
|
+
// whole store and hide every installed extension.
|
|
20
|
+
capabilityGrants: z.record(z.string(), z.array(z.string())).optional(),
|
|
21
|
+
// What each grant covered when the user approved it: the filesystem
|
|
22
|
+
// patterns, the API origin, the service's exec names and socket ids. A
|
|
23
|
+
// newer package that widens any of these is treated as not approved for
|
|
24
|
+
// that capability. Entries are checked one at a time on read.
|
|
25
|
+
capabilityScopes: z.record(z.string(), z.unknown()).optional(),
|
|
26
|
+
disabledGuests: z.record(z.string(), z.literal(true)).optional(),
|
|
27
|
+
serviceSocketOverrides: z.record(
|
|
28
|
+
z.string(),
|
|
29
|
+
z.record(z.string(), z.string().min(1).refine((entry) => !entry.includes('\0'))),
|
|
30
|
+
).optional(),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const capabilityScopeSchema = z.object({
|
|
34
|
+
filesystem: z.array(z.string().min(1)).optional(),
|
|
35
|
+
apiOrigin: z.string().min(1).optional(),
|
|
36
|
+
oauth: z.object({
|
|
37
|
+
authorizeUrl: z.string().min(1),
|
|
38
|
+
tokenUrl: z.string().min(1),
|
|
39
|
+
}).optional(),
|
|
40
|
+
service: z.object({
|
|
41
|
+
exec: z.array(z.string().min(1)),
|
|
42
|
+
sockets: z.array(z.string().min(1)),
|
|
43
|
+
}).optional(),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/** @returns {Record<string, { filesystem?: string[], apiOrigin?: string, service?: { exec: string[], sockets: string[] } }>} */
|
|
47
|
+
const knownScopesOnly = (scopes) => {
|
|
48
|
+
const cleaned = {};
|
|
49
|
+
for (const [guestId, raw] of Object.entries(scopes)) {
|
|
50
|
+
const parsed = capabilityScopeSchema.safeParse(raw);
|
|
51
|
+
if (parsed.success) {
|
|
52
|
+
cleaned[guestId] = parsed.data;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return cleaned;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const gitOriginSchema = z.object({
|
|
59
|
+
url: z.string().min(1).refine((entry) => !entry.includes('\0')),
|
|
60
|
+
ref: z.string().min(1).max(256).refine((entry) => !entry.includes('\0')).optional(),
|
|
61
|
+
gitIdentityId: z.string().trim().min(1).max(128).optional(),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/** @returns {Record<string, { url: string, ref?: string, gitIdentityId?: string }>} */
|
|
65
|
+
const knownGitOriginsOnly = (origins) => {
|
|
66
|
+
/** @type {Record<string, { url: string, ref?: string, gitIdentityId?: string }>} */
|
|
67
|
+
const cleaned = {};
|
|
68
|
+
for (const [installPath, raw] of Object.entries(origins)) {
|
|
69
|
+
const parsed = gitOriginSchema.safeParse(raw);
|
|
70
|
+
if (!parsed.success) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
cleaned[installPath] = parsed.data;
|
|
74
|
+
}
|
|
75
|
+
return cleaned;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const parseStore = (raw) => {
|
|
79
|
+
try {
|
|
80
|
+
const parsed = storeSchema.safeParse(JSON.parse(raw));
|
|
81
|
+
return parsed.success ? parsed.data : null;
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const dataDirSchema = z.string().min(1).refine((entry) => !entry.includes('\0') && path.isAbsolute(entry));
|
|
88
|
+
|
|
89
|
+
/** `{openchamberDataDir}/extensions.json`. One catalog per OpenChamber instance. */
|
|
90
|
+
export const extensionsPersistPath = (dataDir) => {
|
|
91
|
+
const parsed = dataDirSchema.safeParse(dataDir);
|
|
92
|
+
if (!parsed.success) {
|
|
93
|
+
throw new Error('Guest persist needs an absolute OpenChamber data dir');
|
|
94
|
+
}
|
|
95
|
+
return path.join(parsed.data, 'extensions.json');
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** Host-owned zip/git copies. Lives next to `extensions.json` as `{dataDir}/extensions/{id}`. */
|
|
99
|
+
export const guestCopiesDir = (persistPath) => path.join(path.dirname(persistPath), 'extensions');
|
|
100
|
+
|
|
101
|
+
const realOrResolved = (value) => {
|
|
102
|
+
try {
|
|
103
|
+
return fsSync.realpathSync(value);
|
|
104
|
+
} catch {
|
|
105
|
+
return path.resolve(value);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const isCopiedGuestRoot = (root, persistPath) => {
|
|
110
|
+
const copies = realOrResolved(guestCopiesDir(persistPath));
|
|
111
|
+
const resolved = realOrResolved(root);
|
|
112
|
+
return resolved === copies || resolved.startsWith(`${copies}${path.sep}`);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const emptyStore = () => ({
|
|
116
|
+
paths: [],
|
|
117
|
+
sources: {},
|
|
118
|
+
gitOrigins: {},
|
|
119
|
+
capabilityGrants: {},
|
|
120
|
+
capabilityScopes: {},
|
|
121
|
+
disabledGuests: {},
|
|
122
|
+
serviceSocketOverrides: {},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const knownCapabilities = new Set(GUEST_CAPABILITIES);
|
|
126
|
+
|
|
127
|
+
const knownGrantsOnly = (grants) => Object.fromEntries(
|
|
128
|
+
Object.entries(grants).map(([guestId, list]) => [guestId, list.filter((capability) => knownCapabilities.has(capability))]),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
export const readExtensionStore = async (persistPath) => {
|
|
132
|
+
try {
|
|
133
|
+
const raw = await fs.readFile(persistPath, 'utf8');
|
|
134
|
+
const parsed = parseStore(raw);
|
|
135
|
+
if (!parsed) {
|
|
136
|
+
throw new Error('Invalid extensions store');
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
paths: parsed.paths,
|
|
140
|
+
sources: parsed.sources ?? {},
|
|
141
|
+
gitOrigins: knownGitOriginsOnly(parsed.gitOrigins ?? {}),
|
|
142
|
+
capabilityGrants: knownGrantsOnly(parsed.capabilityGrants ?? {}),
|
|
143
|
+
capabilityScopes: knownScopesOnly(parsed.capabilityScopes ?? {}),
|
|
144
|
+
disabledGuests: parsed.disabledGuests ?? {},
|
|
145
|
+
serviceSocketOverrides: parsed.serviceSocketOverrides ?? {},
|
|
146
|
+
};
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
|
149
|
+
return emptyStore();
|
|
150
|
+
}
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const writeExtensionStoreUnlocked = async (
|
|
156
|
+
persistPath,
|
|
157
|
+
{
|
|
158
|
+
paths,
|
|
159
|
+
sources = {},
|
|
160
|
+
gitOrigins = {},
|
|
161
|
+
capabilityGrants = {},
|
|
162
|
+
capabilityScopes = {},
|
|
163
|
+
disabledGuests = {},
|
|
164
|
+
serviceSocketOverrides = {},
|
|
165
|
+
},
|
|
166
|
+
) => {
|
|
167
|
+
const cleaned = {};
|
|
168
|
+
/** @type {Record<string, { url: string, ref?: string }>} */
|
|
169
|
+
const origins = {};
|
|
170
|
+
for (const entry of paths) {
|
|
171
|
+
const source = sources[entry];
|
|
172
|
+
if (source && source !== 'path') {
|
|
173
|
+
cleaned[entry] = source;
|
|
174
|
+
}
|
|
175
|
+
// An origin only means something for a git copy that is still installed.
|
|
176
|
+
const origin = source === 'git' ? gitOrigins[entry] : undefined;
|
|
177
|
+
const parsedOrigin = gitOriginSchema.safeParse(origin);
|
|
178
|
+
if (parsedOrigin.success) {
|
|
179
|
+
origins[entry] = parsedOrigin.data;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const grants = {};
|
|
183
|
+
for (const [id, granted] of Object.entries(capabilityGrants)) {
|
|
184
|
+
if (Array.isArray(granted) && granted.length > 0) {
|
|
185
|
+
grants[id] = [...granted];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const scopes = {};
|
|
189
|
+
for (const [id, scope] of Object.entries(capabilityScopes)) {
|
|
190
|
+
// A scope only means something next to a grant.
|
|
191
|
+
if (grants[id] && capabilityScopeSchema.safeParse(scope).success) {
|
|
192
|
+
scopes[id] = scope;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const disabled = {};
|
|
196
|
+
for (const [id, isDisabled] of Object.entries(disabledGuests)) {
|
|
197
|
+
if (isDisabled) {
|
|
198
|
+
disabled[id] = true;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/** @type {Record<string, Record<string, string>>} */
|
|
202
|
+
const socketOverrides = {};
|
|
203
|
+
for (const [guestId, bySocket] of Object.entries(serviceSocketOverrides)) {
|
|
204
|
+
/** @type {Record<string, string>} */
|
|
205
|
+
const cleanedSockets = {};
|
|
206
|
+
for (const [socketId, socketPath] of Object.entries(bySocket ?? {})) {
|
|
207
|
+
if (typeof socketPath === 'string' && socketPath.trim() && !socketPath.includes('\0')) {
|
|
208
|
+
cleanedSockets[socketId] = socketPath.trim();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (Object.keys(cleanedSockets).length > 0) {
|
|
212
|
+
socketOverrides[guestId] = cleanedSockets;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const payload = { paths };
|
|
216
|
+
if (Object.keys(cleaned).length > 0) {
|
|
217
|
+
payload.sources = cleaned;
|
|
218
|
+
}
|
|
219
|
+
if (Object.keys(origins).length > 0) {
|
|
220
|
+
payload.gitOrigins = origins;
|
|
221
|
+
}
|
|
222
|
+
if (Object.keys(grants).length > 0) {
|
|
223
|
+
payload.capabilityGrants = grants;
|
|
224
|
+
}
|
|
225
|
+
if (Object.keys(scopes).length > 0) {
|
|
226
|
+
payload.capabilityScopes = scopes;
|
|
227
|
+
}
|
|
228
|
+
if (Object.keys(disabled).length > 0) {
|
|
229
|
+
payload.disabledGuests = disabled;
|
|
230
|
+
}
|
|
231
|
+
if (Object.keys(socketOverrides).length > 0) {
|
|
232
|
+
payload.serviceSocketOverrides = socketOverrides;
|
|
233
|
+
}
|
|
234
|
+
// Listeners hear about a write twice: before, so a cached catalog is
|
|
235
|
+
// dropped, and after, so a read that started in between (and saw the old
|
|
236
|
+
// file) cannot be cached as current.
|
|
237
|
+
for (const listener of writeListeners) listener(persistPath);
|
|
238
|
+
await fs.mkdir(path.dirname(persistPath), { recursive: true });
|
|
239
|
+
const tmp = `${persistPath}.tmp-${process.pid}-${Date.now()}-${(writeSequence += 1)}`;
|
|
240
|
+
await fs.writeFile(tmp, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
241
|
+
await fs.rename(tmp, persistPath);
|
|
242
|
+
for (const listener of writeListeners) listener(persistPath);
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
/** Replace the whole store. Prefer `updateExtensionStore` for a change based on the current contents. */
|
|
246
|
+
export const writeExtensionStore = (persistPath, store) => (
|
|
247
|
+
withStoreWriteLock(persistPath, () => writeExtensionStoreUnlocked(persistPath, store))
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Read, change, and write the store as one step under the write lock, so two
|
|
252
|
+
* changes made at the same time (Allow in one window, Pause in another) both
|
|
253
|
+
* land instead of the second one writing over a stale copy of the first.
|
|
254
|
+
* `mutate` returns the next store, or `null` to leave the file alone.
|
|
255
|
+
* @param {string} persistPath
|
|
256
|
+
* @param {(current: Awaited<ReturnType<typeof readExtensionStore>>) => Promise<object | null> | object | null} mutate
|
|
257
|
+
*/
|
|
258
|
+
export const updateExtensionStore = (persistPath, mutate) => (
|
|
259
|
+
withStoreWriteLock(persistPath, async () => {
|
|
260
|
+
const current = await readExtensionStore(persistPath);
|
|
261
|
+
const next = await mutate(current);
|
|
262
|
+
if (next) {
|
|
263
|
+
await writeExtensionStoreUnlocked(persistPath, next);
|
|
264
|
+
}
|
|
265
|
+
})
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
let writeSequence = 0;
|
|
269
|
+
/** @type {Set<(persistPath: string) => void>} */
|
|
270
|
+
const writeListeners = new Set();
|
|
271
|
+
|
|
272
|
+
/** Called before and after every store write with the path that changes. */
|
|
273
|
+
export const onExtensionStoreWrite = (listener) => {
|
|
274
|
+
writeListeners.add(listener);
|
|
275
|
+
return () => writeListeners.delete(listener);
|
|
276
|
+
};
|
|
277
|
+
/** @type {Map<string, Promise<void>>} */
|
|
278
|
+
const writeChains = new Map();
|
|
279
|
+
|
|
280
|
+
// Two writers in one process (say, Enable and Allow clicked back to back)
|
|
281
|
+
// used to share one temp file: the second truncated it under the first's
|
|
282
|
+
// rename and the store came back as half a JSON document. Writes to one path
|
|
283
|
+
// now run one after another, each on its own temp file.
|
|
284
|
+
const withStoreWriteLock = (persistPath, write) => {
|
|
285
|
+
const previous = writeChains.get(persistPath) ?? Promise.resolve();
|
|
286
|
+
const next = previous.catch(() => undefined).then(write);
|
|
287
|
+
writeChains.set(persistPath, next.finally(() => {
|
|
288
|
+
if (writeChains.get(persistPath) === chained) writeChains.delete(persistPath);
|
|
289
|
+
}));
|
|
290
|
+
const chained = writeChains.get(persistPath);
|
|
291
|
+
return next;
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
export const readExtensionPaths = async (persistPath) => {
|
|
295
|
+
const store = await readExtensionStore(persistPath);
|
|
296
|
+
return store.paths;
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
export const writeExtensionPaths = (paths, persistPath) => updateExtensionStore(persistPath, (current) => {
|
|
300
|
+
const sources = {};
|
|
301
|
+
const gitOrigins = {};
|
|
302
|
+
for (const entry of paths) {
|
|
303
|
+
if (current.sources[entry]) {
|
|
304
|
+
sources[entry] = current.sources[entry];
|
|
305
|
+
}
|
|
306
|
+
if (current.gitOrigins[entry]) {
|
|
307
|
+
gitOrigins[entry] = current.gitOrigins[entry];
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return { ...current, paths, sources, gitOrigins };
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Record the user's approval for one guest. `granted` replaces the previous
|
|
315
|
+
* list; an empty list withdraws approval. Callers pass the requested list
|
|
316
|
+
* verbatim, so approval is all-or-nothing per install.
|
|
317
|
+
* @param {string} guestId
|
|
318
|
+
* @param {string} persistPath
|
|
319
|
+
* @param {string[]} granted
|
|
320
|
+
*/
|
|
321
|
+
export const setCapabilityGrants = (guestId, persistPath, granted, scope = null) => updateExtensionStore(persistPath, (current) => {
|
|
322
|
+
const capabilityGrants = { ...current.capabilityGrants };
|
|
323
|
+
const capabilityScopes = { ...current.capabilityScopes };
|
|
324
|
+
if (granted.length > 0) {
|
|
325
|
+
capabilityGrants[guestId] = [...granted];
|
|
326
|
+
if (scope) {
|
|
327
|
+
capabilityScopes[guestId] = scope;
|
|
328
|
+
} else {
|
|
329
|
+
delete capabilityScopes[guestId];
|
|
330
|
+
}
|
|
331
|
+
} else {
|
|
332
|
+
delete capabilityGrants[guestId];
|
|
333
|
+
delete capabilityScopes[guestId];
|
|
334
|
+
}
|
|
335
|
+
return { ...current, capabilityGrants, capabilityScopes };
|
|
336
|
+
});
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
extensionsPersistPath,
|
|
8
|
+
guestCopiesDir,
|
|
9
|
+
isCopiedGuestRoot,
|
|
10
|
+
readExtensionPaths,
|
|
11
|
+
setCapabilityGrants,
|
|
12
|
+
readExtensionStore,
|
|
13
|
+
writeExtensionPaths,
|
|
14
|
+
writeExtensionStore,
|
|
15
|
+
} from './persist.js';
|
|
16
|
+
|
|
17
|
+
describe('extensionsPersistPath', () => {
|
|
18
|
+
test('joins the instance data dir and refuses a relative path', () => {
|
|
19
|
+
const dataDir = path.join(os.tmpdir(), 'oc-instance-a');
|
|
20
|
+
expect(extensionsPersistPath(dataDir)).toBe(path.join(dataDir, 'extensions.json'));
|
|
21
|
+
expect(() => extensionsPersistPath('relative')).toThrow('absolute OpenChamber data dir');
|
|
22
|
+
expect(() => extensionsPersistPath('')).toThrow('absolute OpenChamber data dir');
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('extension persist', () => {
|
|
27
|
+
test('round-trips paths and treats a missing file as empty', async () => {
|
|
28
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-ext-'));
|
|
29
|
+
const file = extensionsPersistPath(dir);
|
|
30
|
+
expect(await readExtensionPaths(file)).toEqual([]);
|
|
31
|
+
await writeExtensionPaths(['/one', '/two'], file);
|
|
32
|
+
expect(await readExtensionPaths(file)).toEqual(['/one', '/two']);
|
|
33
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('keeps two instance stores apart', async () => {
|
|
37
|
+
const parent = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-ext-'));
|
|
38
|
+
const a = extensionsPersistPath(path.join(parent, 'a'));
|
|
39
|
+
const b = extensionsPersistPath(path.join(parent, 'b'));
|
|
40
|
+
await writeExtensionPaths(['/one'], a);
|
|
41
|
+
await writeExtensionPaths(['/two'], b);
|
|
42
|
+
expect(await readExtensionPaths(a)).toEqual(['/one']);
|
|
43
|
+
expect(await readExtensionPaths(b)).toEqual(['/two']);
|
|
44
|
+
await fs.rm(parent, { recursive: true, force: true });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('reads a path-only store and keeps zip sources', async () => {
|
|
48
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-ext-'));
|
|
49
|
+
const file = extensionsPersistPath(dir);
|
|
50
|
+
await fs.writeFile(file, `${JSON.stringify({ paths: ['/one'] })}\n`, 'utf8');
|
|
51
|
+
expect(await readExtensionStore(file)).toEqual({
|
|
52
|
+
paths: ['/one'],
|
|
53
|
+
sources: {},
|
|
54
|
+
gitOrigins: {},
|
|
55
|
+
capabilityGrants: {},
|
|
56
|
+
capabilityScopes: {},
|
|
57
|
+
disabledGuests: {},
|
|
58
|
+
serviceSocketOverrides: {},
|
|
59
|
+
});
|
|
60
|
+
await writeExtensionStore(file, {
|
|
61
|
+
paths: ['/one', '/two'],
|
|
62
|
+
sources: { '/one': 'path', '/two': 'zip' },
|
|
63
|
+
});
|
|
64
|
+
expect(await readExtensionStore(file)).toEqual({
|
|
65
|
+
paths: ['/one', '/two'],
|
|
66
|
+
sources: { '/two': 'zip' },
|
|
67
|
+
gitOrigins: {},
|
|
68
|
+
capabilityGrants: {},
|
|
69
|
+
capabilityScopes: {},
|
|
70
|
+
disabledGuests: {},
|
|
71
|
+
serviceSocketOverrides: {},
|
|
72
|
+
});
|
|
73
|
+
await writeExtensionPaths(['/two'], file);
|
|
74
|
+
expect(await readExtensionStore(file)).toEqual({
|
|
75
|
+
paths: ['/two'],
|
|
76
|
+
sources: { '/two': 'zip' },
|
|
77
|
+
gitOrigins: {},
|
|
78
|
+
capabilityGrants: {},
|
|
79
|
+
capabilityScopes: {},
|
|
80
|
+
disabledGuests: {},
|
|
81
|
+
serviceSocketOverrides: {},
|
|
82
|
+
});
|
|
83
|
+
expect(guestCopiesDir(file)).toBe(path.join(dir, 'extensions'));
|
|
84
|
+
expect(isCopiedGuestRoot(path.join(dir, 'extensions', 'hello'), file)).toBe(true);
|
|
85
|
+
expect(isCopiedGuestRoot(path.join(dir, 'other', 'hello'), file)).toBe(false);
|
|
86
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('round-trips service grants', async () => {
|
|
90
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-ext-'));
|
|
91
|
+
const file = extensionsPersistPath(dir);
|
|
92
|
+
await writeExtensionStore(file, {
|
|
93
|
+
paths: ['/one'],
|
|
94
|
+
sources: {},
|
|
95
|
+
gitOrigins: {},
|
|
96
|
+
capabilityGrants: { docker: ['service'] },
|
|
97
|
+
});
|
|
98
|
+
expect(await readExtensionStore(file)).toEqual({
|
|
99
|
+
paths: ['/one'],
|
|
100
|
+
sources: {},
|
|
101
|
+
gitOrigins: {},
|
|
102
|
+
capabilityGrants: { docker: ['service'] },
|
|
103
|
+
capabilityScopes: {},
|
|
104
|
+
disabledGuests: {},
|
|
105
|
+
serviceSocketOverrides: {},
|
|
106
|
+
});
|
|
107
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('round-trips service socket overrides', async () => {
|
|
111
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-ext-'));
|
|
112
|
+
const file = extensionsPersistPath(dir);
|
|
113
|
+
await writeExtensionStore(file, {
|
|
114
|
+
paths: ['/one'],
|
|
115
|
+
sources: {},
|
|
116
|
+
gitOrigins: {},
|
|
117
|
+
capabilityGrants: { docker: ['service'] },
|
|
118
|
+
serviceSocketOverrides: { docker: { docker: '/custom/docker.sock' } },
|
|
119
|
+
});
|
|
120
|
+
expect(await readExtensionStore(file)).toEqual({
|
|
121
|
+
paths: ['/one'],
|
|
122
|
+
sources: {},
|
|
123
|
+
gitOrigins: {},
|
|
124
|
+
capabilityGrants: { docker: ['service'] },
|
|
125
|
+
capabilityScopes: {},
|
|
126
|
+
disabledGuests: {},
|
|
127
|
+
serviceSocketOverrides: { docker: { docker: '/custom/docker.sock' } },
|
|
128
|
+
});
|
|
129
|
+
await writeExtensionPaths(['/one'], file);
|
|
130
|
+
expect(await readExtensionStore(file)).toEqual({
|
|
131
|
+
paths: ['/one'],
|
|
132
|
+
sources: {},
|
|
133
|
+
gitOrigins: {},
|
|
134
|
+
capabilityGrants: { docker: ['service'] },
|
|
135
|
+
capabilityScopes: {},
|
|
136
|
+
disabledGuests: {},
|
|
137
|
+
serviceSocketOverrides: { docker: { docker: '/custom/docker.sock' } },
|
|
138
|
+
});
|
|
139
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('round-trips disabled guests', async () => {
|
|
143
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-ext-'));
|
|
144
|
+
const file = extensionsPersistPath(dir);
|
|
145
|
+
await writeExtensionStore(file, {
|
|
146
|
+
paths: ['/one'],
|
|
147
|
+
sources: {},
|
|
148
|
+
disabledGuests: { docker: true },
|
|
149
|
+
});
|
|
150
|
+
expect(await readExtensionStore(file)).toEqual({
|
|
151
|
+
paths: ['/one'],
|
|
152
|
+
sources: {},
|
|
153
|
+
gitOrigins: {},
|
|
154
|
+
capabilityGrants: {},
|
|
155
|
+
capabilityScopes: {},
|
|
156
|
+
disabledGuests: { docker: true },
|
|
157
|
+
serviceSocketOverrides: {},
|
|
158
|
+
});
|
|
159
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('keeps the store whole under concurrent writes', async () => {
|
|
163
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-guest-persist-'));
|
|
164
|
+
const file = extensionsPersistPath(dir);
|
|
165
|
+
try {
|
|
166
|
+
await Promise.all(Array.from({ length: 12 }, (_, index) => (
|
|
167
|
+
writeExtensionStore(file, { paths: [`/guests/${index}`] })
|
|
168
|
+
)));
|
|
169
|
+
const store = await readExtensionStore(file);
|
|
170
|
+
expect(store.paths).toHaveLength(1);
|
|
171
|
+
} finally {
|
|
172
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('drops grants this build no longer knows instead of refusing the store', async () => {
|
|
177
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-ext-'));
|
|
178
|
+
const persistPath = extensionsPersistPath(dir);
|
|
179
|
+
await fs.writeFile(persistPath, JSON.stringify({
|
|
180
|
+
paths: ['/tmp/old-echo', '/tmp/tasks'],
|
|
181
|
+
capabilityGrants: { 'old-echo': ['agent'], tasks: ['prompt', 'made-up', 'sessions'] },
|
|
182
|
+
}));
|
|
183
|
+
try {
|
|
184
|
+
const store = await readExtensionStore(persistPath);
|
|
185
|
+
expect(store.paths).toEqual(['/tmp/old-echo', '/tmp/tasks']);
|
|
186
|
+
expect(store.capabilityGrants).toEqual({ 'old-echo': [], tasks: ['prompt', 'sessions'] });
|
|
187
|
+
} finally {
|
|
188
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('refuses a corrupt store', async () => {
|
|
193
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-ext-'));
|
|
194
|
+
const file = extensionsPersistPath(dir);
|
|
195
|
+
await fs.writeFile(file, '{"paths":[1]}', 'utf8');
|
|
196
|
+
try {
|
|
197
|
+
await readExtensionPaths(file);
|
|
198
|
+
throw new Error('should have thrown');
|
|
199
|
+
} catch (error) {
|
|
200
|
+
expect(error).toBeInstanceOf(Error);
|
|
201
|
+
expect(String(error)).toContain('Invalid extensions store');
|
|
202
|
+
}
|
|
203
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('keeps git origins for installed git copies and drops malformed ones', async () => {
|
|
207
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-ext-'));
|
|
208
|
+
const file = extensionsPersistPath(dir);
|
|
209
|
+
await writeExtensionStore(file, {
|
|
210
|
+
paths: ['/git', '/zip', '/folder'],
|
|
211
|
+
sources: { '/git': 'git', '/zip': 'zip' },
|
|
212
|
+
gitOrigins: {
|
|
213
|
+
'/git': { url: 'https://github.com/acme/panel.git', ref: 'v1' },
|
|
214
|
+
'/zip': { url: 'https://example.com/ignored.git' },
|
|
215
|
+
'/folder': { url: 'https://example.com/ignored.git' },
|
|
216
|
+
'/gone': { url: 'https://example.com/ignored.git' },
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
expect((await readExtensionStore(file)).gitOrigins).toEqual({
|
|
220
|
+
'/git': { url: 'https://github.com/acme/panel.git', ref: 'v1' },
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
await writeExtensionPaths(['/zip'], file);
|
|
224
|
+
expect((await readExtensionStore(file)).gitOrigins).toEqual({});
|
|
225
|
+
|
|
226
|
+
await fs.writeFile(file, `${JSON.stringify({
|
|
227
|
+
paths: ['/a', '/b'],
|
|
228
|
+
sources: { '/a': 'git', '/b': 'git' },
|
|
229
|
+
gitOrigins: { '/a': { url: 'https://github.com/acme/a.git' }, '/b': { ref: 'main' }, '/c': 'nope' },
|
|
230
|
+
})}\n`, 'utf8');
|
|
231
|
+
const tolerant = await readExtensionStore(file);
|
|
232
|
+
expect(tolerant.paths).toEqual(['/a', '/b']);
|
|
233
|
+
expect(tolerant.gitOrigins).toEqual({ '/a': { url: 'https://github.com/acme/a.git' } });
|
|
234
|
+
|
|
235
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe('concurrent store changes', () => {
|
|
240
|
+
test('two approvals at once both land', async () => {
|
|
241
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-guest-persist-'));
|
|
242
|
+
const file = extensionsPersistPath(dir);
|
|
243
|
+
try {
|
|
244
|
+
await writeExtensionStore(file, { paths: ['/a', '/b'] });
|
|
245
|
+
await Promise.all([
|
|
246
|
+
setCapabilityGrants('alpha', file, ['prompt'], null),
|
|
247
|
+
setCapabilityGrants('beta', file, ['filesystem'], { filesystem: ['~/notes/**'] }),
|
|
248
|
+
]);
|
|
249
|
+
const store = await readExtensionStore(file);
|
|
250
|
+
expect(store.capabilityGrants).toEqual({ alpha: ['prompt'], beta: ['filesystem'] });
|
|
251
|
+
expect(store.capabilityScopes).toEqual({ beta: { filesystem: ['~/notes/**'] } });
|
|
252
|
+
} finally {
|
|
253
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test('withdrawing approval also drops the recorded scope', async () => {
|
|
258
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-guest-persist-'));
|
|
259
|
+
const file = extensionsPersistPath(dir);
|
|
260
|
+
try {
|
|
261
|
+
await writeExtensionStore(file, { paths: ['/a'] });
|
|
262
|
+
await setCapabilityGrants('alpha', file, ['network'], { apiOrigin: 'https://api.example' });
|
|
263
|
+
expect((await readExtensionStore(file)).capabilityScopes).toEqual({ alpha: { apiOrigin: 'https://api.example' } });
|
|
264
|
+
await setCapabilityGrants('alpha', file, []);
|
|
265
|
+
const store = await readExtensionStore(file);
|
|
266
|
+
expect(store.capabilityGrants).toEqual({});
|
|
267
|
+
expect(store.capabilityScopes).toEqual({});
|
|
268
|
+
} finally {
|
|
269
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('drops a malformed scope entry on read instead of refusing the store', async () => {
|
|
274
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-guest-persist-'));
|
|
275
|
+
const file = extensionsPersistPath(dir);
|
|
276
|
+
try {
|
|
277
|
+
await fs.writeFile(file, JSON.stringify({
|
|
278
|
+
paths: ['/a'],
|
|
279
|
+
capabilityGrants: { alpha: ['filesystem'], beta: ['network'] },
|
|
280
|
+
capabilityScopes: { alpha: { filesystem: 'not-a-list' }, beta: { apiOrigin: 'https://api.example' } },
|
|
281
|
+
}));
|
|
282
|
+
const store = await readExtensionStore(file);
|
|
283
|
+
expect(store.capabilityScopes).toEqual({ beta: { apiOrigin: 'https://api.example' } });
|
|
284
|
+
} finally {
|
|
285
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
});
|