@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,136 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { inflateRawSync } from 'node:zlib';
|
|
4
|
+
|
|
5
|
+
const LOCAL_FILE = 0x04034b50;
|
|
6
|
+
const CENTRAL_DIR = 0x02014b50;
|
|
7
|
+
const MAX_FILES = 500;
|
|
8
|
+
const MAX_UNCOMPRESSED = 40 * 1024 * 1024;
|
|
9
|
+
const COMPRESSION_STORE = 0;
|
|
10
|
+
const COMPRESSION_DEFLATE = 8;
|
|
11
|
+
|
|
12
|
+
const readU16 = (buffer, offset) => buffer.readUInt16LE(offset);
|
|
13
|
+
const readU32 = (buffer, offset) => buffer.readUInt32LE(offset);
|
|
14
|
+
|
|
15
|
+
const isSafeZipName = (name) => {
|
|
16
|
+
if (!name || name.includes('\0') || name.includes('\\')) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
if (name.startsWith('/') || name.startsWith('./')) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const segments = name.split('/');
|
|
23
|
+
return !segments.some((segment) => segment === '..');
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Extract a zip buffer into dest. Rejects traversal, zip64, encryption, and data descriptors. */
|
|
27
|
+
export const extractZipBuffer = async (buffer, destRoot) => {
|
|
28
|
+
if (!Buffer.isBuffer(buffer) || buffer.length < 22) {
|
|
29
|
+
return { ok: false, code: 'extract-failed' };
|
|
30
|
+
}
|
|
31
|
+
await fs.mkdir(destRoot, { recursive: true });
|
|
32
|
+
const destReal = await fs.realpath(destRoot);
|
|
33
|
+
let offset = 0;
|
|
34
|
+
let files = 0;
|
|
35
|
+
let uncompressedTotal = 0;
|
|
36
|
+
|
|
37
|
+
while (offset + 4 <= buffer.length) {
|
|
38
|
+
const signature = readU32(buffer, offset);
|
|
39
|
+
if (signature === CENTRAL_DIR) {
|
|
40
|
+
return { ok: true };
|
|
41
|
+
}
|
|
42
|
+
if (signature !== LOCAL_FILE) {
|
|
43
|
+
return { ok: false, code: 'extract-failed' };
|
|
44
|
+
}
|
|
45
|
+
if (offset + 30 > buffer.length) {
|
|
46
|
+
return { ok: false, code: 'extract-failed' };
|
|
47
|
+
}
|
|
48
|
+
const flags = readU16(buffer, offset + 6);
|
|
49
|
+
const method = readU16(buffer, offset + 8);
|
|
50
|
+
const compressedSize = readU32(buffer, offset + 18);
|
|
51
|
+
const uncompressedSize = readU32(buffer, offset + 22);
|
|
52
|
+
const nameLen = readU16(buffer, offset + 26);
|
|
53
|
+
const extraLen = readU16(buffer, offset + 28);
|
|
54
|
+
if ((flags & 0x0001) !== 0 || (flags & 0x0008) !== 0) {
|
|
55
|
+
return { ok: false, code: 'extract-failed' };
|
|
56
|
+
}
|
|
57
|
+
if (method !== COMPRESSION_STORE && method !== COMPRESSION_DEFLATE) {
|
|
58
|
+
return { ok: false, code: 'extract-failed' };
|
|
59
|
+
}
|
|
60
|
+
if (compressedSize === 0xffffffff || uncompressedSize === 0xffffffff) {
|
|
61
|
+
return { ok: false, code: 'extract-failed' };
|
|
62
|
+
}
|
|
63
|
+
const nameStart = offset + 30;
|
|
64
|
+
const nameEnd = nameStart + nameLen;
|
|
65
|
+
const dataStart = nameEnd + extraLen;
|
|
66
|
+
const dataEnd = dataStart + compressedSize;
|
|
67
|
+
if (dataEnd > buffer.length) {
|
|
68
|
+
return { ok: false, code: 'extract-failed' };
|
|
69
|
+
}
|
|
70
|
+
const name = buffer.subarray(nameStart, nameEnd).toString('utf8');
|
|
71
|
+
offset = dataEnd;
|
|
72
|
+
if (name.endsWith('/')) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (!isSafeZipName(name)) {
|
|
76
|
+
return { ok: false, code: 'extract-failed' };
|
|
77
|
+
}
|
|
78
|
+
files += 1;
|
|
79
|
+
uncompressedTotal += uncompressedSize;
|
|
80
|
+
if (files > MAX_FILES || uncompressedTotal > MAX_UNCOMPRESSED) {
|
|
81
|
+
return { ok: false, code: 'extract-failed' };
|
|
82
|
+
}
|
|
83
|
+
let data;
|
|
84
|
+
try {
|
|
85
|
+
const payload = buffer.subarray(dataStart, dataEnd);
|
|
86
|
+
// The declared size is already within the archive budget; capping the
|
|
87
|
+
// inflate at that size keeps a tiny deflate stream from expanding into
|
|
88
|
+
// gigabytes before the length check below can reject it.
|
|
89
|
+
data = method === COMPRESSION_STORE
|
|
90
|
+
? payload
|
|
91
|
+
: inflateRawSync(payload, { maxOutputLength: Math.max(1, uncompressedSize) });
|
|
92
|
+
} catch {
|
|
93
|
+
return { ok: false, code: 'extract-failed' };
|
|
94
|
+
}
|
|
95
|
+
if (data.length !== uncompressedSize) {
|
|
96
|
+
return { ok: false, code: 'extract-failed' };
|
|
97
|
+
}
|
|
98
|
+
const target = path.resolve(destReal, ...name.split('/'));
|
|
99
|
+
if (target !== destReal && !target.startsWith(destReal + path.sep)) {
|
|
100
|
+
return { ok: false, code: 'extract-failed' };
|
|
101
|
+
}
|
|
102
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
103
|
+
await fs.writeFile(target, data);
|
|
104
|
+
}
|
|
105
|
+
return { ok: false, code: 'extract-failed' };
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const exists = async (filePath) => {
|
|
109
|
+
try {
|
|
110
|
+
await fs.stat(filePath);
|
|
111
|
+
return true;
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/** If the extract has one root folder and package.json is inside it, use that folder. */
|
|
118
|
+
export const unwrapGuestRoot = async (extracted) => {
|
|
119
|
+
if (await exists(path.join(extracted, 'package.json'))) {
|
|
120
|
+
return extracted;
|
|
121
|
+
}
|
|
122
|
+
const names = await fs.readdir(extracted);
|
|
123
|
+
if (names.length !== 1) {
|
|
124
|
+
return extracted;
|
|
125
|
+
}
|
|
126
|
+
const inner = path.join(extracted, names[0]);
|
|
127
|
+
try {
|
|
128
|
+
const stat = await fs.stat(inner);
|
|
129
|
+
if (stat.isDirectory() && await exists(path.join(inner, 'package.json'))) {
|
|
130
|
+
return inner;
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
return extracted;
|
|
134
|
+
}
|
|
135
|
+
return extracted;
|
|
136
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { crc32 } from 'node:zlib';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
import { extractZipBuffer, unwrapGuestRoot } from './extract-zip.js';
|
|
8
|
+
|
|
9
|
+
const u16 = (value) => {
|
|
10
|
+
const buffer = Buffer.alloc(2);
|
|
11
|
+
buffer.writeUInt16LE(value);
|
|
12
|
+
return buffer;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const u32 = (value) => {
|
|
16
|
+
const buffer = Buffer.alloc(4);
|
|
17
|
+
buffer.writeUInt32LE(value);
|
|
18
|
+
return buffer;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Store-only zip. Used by install tests too. */
|
|
22
|
+
export const buildStoreZip = (files) => {
|
|
23
|
+
const locals = [];
|
|
24
|
+
const centrals = [];
|
|
25
|
+
let offset = 0;
|
|
26
|
+
for (const file of files) {
|
|
27
|
+
const name = Buffer.from(file.name, 'utf8');
|
|
28
|
+
const data = Buffer.isBuffer(file.data) ? file.data : Buffer.from(file.data);
|
|
29
|
+
const crc = crc32(data);
|
|
30
|
+
const local = Buffer.concat([
|
|
31
|
+
u32(0x04034b50),
|
|
32
|
+
u16(20),
|
|
33
|
+
u16(0),
|
|
34
|
+
u16(0),
|
|
35
|
+
u16(0),
|
|
36
|
+
u16(0),
|
|
37
|
+
u32(crc),
|
|
38
|
+
u32(data.length),
|
|
39
|
+
u32(data.length),
|
|
40
|
+
u16(name.length),
|
|
41
|
+
u16(0),
|
|
42
|
+
name,
|
|
43
|
+
data,
|
|
44
|
+
]);
|
|
45
|
+
locals.push(local);
|
|
46
|
+
centrals.push(Buffer.concat([
|
|
47
|
+
u32(0x02014b50),
|
|
48
|
+
u16(20),
|
|
49
|
+
u16(20),
|
|
50
|
+
u16(0),
|
|
51
|
+
u16(0),
|
|
52
|
+
u16(0),
|
|
53
|
+
u16(0),
|
|
54
|
+
u32(crc),
|
|
55
|
+
u32(data.length),
|
|
56
|
+
u32(data.length),
|
|
57
|
+
u16(name.length),
|
|
58
|
+
u16(0),
|
|
59
|
+
u16(0),
|
|
60
|
+
u16(0),
|
|
61
|
+
u16(0),
|
|
62
|
+
u32(0),
|
|
63
|
+
u32(offset),
|
|
64
|
+
name,
|
|
65
|
+
]));
|
|
66
|
+
offset += local.length;
|
|
67
|
+
}
|
|
68
|
+
const central = Buffer.concat(centrals);
|
|
69
|
+
const eocd = Buffer.concat([
|
|
70
|
+
u32(0x06054b50),
|
|
71
|
+
u16(0),
|
|
72
|
+
u16(0),
|
|
73
|
+
u16(files.length),
|
|
74
|
+
u16(files.length),
|
|
75
|
+
u32(central.length),
|
|
76
|
+
u32(offset),
|
|
77
|
+
u16(0),
|
|
78
|
+
]);
|
|
79
|
+
return Buffer.concat([...locals, central, eocd]);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
describe('extractZipBuffer', () => {
|
|
83
|
+
test('extracts files and rejects traversal', async () => {
|
|
84
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-zip-'));
|
|
85
|
+
const ok = await extractZipBuffer(buildStoreZip([
|
|
86
|
+
{ name: 'package.json', data: '{"ok":true}' },
|
|
87
|
+
{ name: 'panel/index.html', data: '<html></html>' },
|
|
88
|
+
]), dir);
|
|
89
|
+
expect(ok).toEqual({ ok: true });
|
|
90
|
+
expect(await fs.readFile(path.join(dir, 'package.json'), 'utf8')).toBe('{"ok":true}');
|
|
91
|
+
expect(await fs.readFile(path.join(dir, 'panel', 'index.html'), 'utf8')).toBe('<html></html>');
|
|
92
|
+
|
|
93
|
+
const escapeDir = path.join(dir, 'escape');
|
|
94
|
+
const escaped = await extractZipBuffer(buildStoreZip([
|
|
95
|
+
{ name: '../secret', data: 'nope' },
|
|
96
|
+
]), escapeDir);
|
|
97
|
+
expect(escaped).toEqual({ ok: false, code: 'extract-failed' });
|
|
98
|
+
|
|
99
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('unwraps a single root folder', async () => {
|
|
103
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-zip-'));
|
|
104
|
+
await fs.mkdir(path.join(dir, 'panel-root', 'panel'), { recursive: true });
|
|
105
|
+
await fs.writeFile(path.join(dir, 'panel-root', 'package.json'), '{}');
|
|
106
|
+
expect(await unwrapGuestRoot(dir)).toBe(path.join(dir, 'panel-root'));
|
|
107
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import defaultFs from 'node:fs/promises';
|
|
2
|
+
import defaultPath from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { GUEST_FILE_CONTENT_MAX, GUEST_FILE_LIST_MAX, GUEST_FILE_PATH_MAX, guestFileScope } from '@openchamber/sdk';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Guest file access. A relative path lives inside the open project and needs
|
|
8
|
+
* the `files` grant. `/…` and `~/…` are outside the project, must match one
|
|
9
|
+
* of the package's `contributes.filesystem` patterns, and need the
|
|
10
|
+
* `filesystem` grant. Every comparison is made on canonical absolute paths so
|
|
11
|
+
* a symlink cannot widen either scope.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const failure = (code, message) => ({ ok: false, code, message });
|
|
15
|
+
|
|
16
|
+
const isGlobChar = (segment) => segment.includes('*') || segment.includes('?');
|
|
17
|
+
|
|
18
|
+
const escapeRegExp = (value) => value.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
19
|
+
|
|
20
|
+
/** Forward-slash form of a path so a glob written with `/` matches on every platform. */
|
|
21
|
+
const toPosix = (value, nodePath) => (nodePath.sep === '/' ? value : value.split(nodePath.sep).join('/'));
|
|
22
|
+
|
|
23
|
+
export const expandHome = (pattern, homeDir) => {
|
|
24
|
+
if (pattern === '~') {
|
|
25
|
+
return homeDir;
|
|
26
|
+
}
|
|
27
|
+
if (pattern.startsWith('~/')) {
|
|
28
|
+
return `${homeDir.replace(/[\\/]+$/, '')}/${pattern.slice(2)}`;
|
|
29
|
+
}
|
|
30
|
+
return pattern;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* `**` spans any number of segments, `*` and `?` stay inside one segment. A
|
|
35
|
+
* pattern that ends in `/**` also matches the directory itself, so a guest
|
|
36
|
+
* may list the root it was granted.
|
|
37
|
+
*/
|
|
38
|
+
export const globToRegExp = (pattern) => {
|
|
39
|
+
let source = '^';
|
|
40
|
+
let index = 0;
|
|
41
|
+
while (index < pattern.length) {
|
|
42
|
+
const char = pattern[index];
|
|
43
|
+
if (char === '*' && pattern[index + 1] === '*') {
|
|
44
|
+
const afterSlash = index === 0 || pattern[index - 1] === '/';
|
|
45
|
+
const beforeSlash = pattern[index + 2] === '/';
|
|
46
|
+
const atEnd = index + 2 === pattern.length;
|
|
47
|
+
if (afterSlash && beforeSlash) {
|
|
48
|
+
source += '(?:.*/)?';
|
|
49
|
+
index += 3;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (afterSlash && atEnd && source.endsWith('/')) {
|
|
53
|
+
source = `${source.slice(0, -1)}(?:/.*)?`;
|
|
54
|
+
index += 2;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
source += '.*';
|
|
58
|
+
index += 2;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (char === '*') {
|
|
62
|
+
source += '[^/]*';
|
|
63
|
+
index += 1;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (char === '?') {
|
|
67
|
+
source += '[^/]';
|
|
68
|
+
index += 1;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
source += escapeRegExp(char);
|
|
72
|
+
index += 1;
|
|
73
|
+
}
|
|
74
|
+
return new RegExp(`${source}$`);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const matchesFilesystemPattern = (canonicalPath, patterns, homeDir, nodePath = defaultPath) => {
|
|
78
|
+
const candidate = toPosix(canonicalPath, nodePath);
|
|
79
|
+
return patterns.some((pattern) => globToRegExp(expandHome(pattern, homeDir)).test(candidate));
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Realpath of the deepest existing ancestor plus the missing tail, so a path
|
|
84
|
+
* that does not exist yet (a write) still resolves through the symlinks above
|
|
85
|
+
* it. Permission errors propagate; the caller maps them to `DENIED`.
|
|
86
|
+
*/
|
|
87
|
+
const canonicalize = async (absolute, fsPromises, nodePath) => {
|
|
88
|
+
let current = absolute;
|
|
89
|
+
const tail = [];
|
|
90
|
+
for (;;) {
|
|
91
|
+
try {
|
|
92
|
+
const real = await fsPromises.realpath(current);
|
|
93
|
+
return tail.length > 0 ? nodePath.join(real, ...tail) : real;
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (error?.code !== 'ENOENT' && error?.code !== 'ENOTDIR') {
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const parent = nodePath.dirname(current);
|
|
100
|
+
if (parent === current) {
|
|
101
|
+
return absolute;
|
|
102
|
+
}
|
|
103
|
+
tail.unshift(nodePath.basename(current));
|
|
104
|
+
current = parent;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The literal prefix of a pattern (segments before the first glob) goes
|
|
110
|
+
* through the same canonicalization as the candidate, so `/tmp/**` still
|
|
111
|
+
* matches on a host where `/tmp` is a symlink to `/private/tmp`.
|
|
112
|
+
*/
|
|
113
|
+
const canonicalizePattern = async (pattern, homeDir, fsPromises, nodePath) => {
|
|
114
|
+
const expanded = expandHome(pattern, homeDir);
|
|
115
|
+
const segments = expanded.split('/');
|
|
116
|
+
const firstGlob = segments.findIndex(isGlobChar);
|
|
117
|
+
const literal = firstGlob === -1 ? segments : segments.slice(0, firstGlob);
|
|
118
|
+
const rest = firstGlob === -1 ? [] : segments.slice(firstGlob);
|
|
119
|
+
const literalPath = literal.join('/') || '/';
|
|
120
|
+
const canonical = toPosix(await canonicalize(nodePath.resolve(literalPath), fsPromises, nodePath), nodePath);
|
|
121
|
+
return rest.length > 0 ? `${canonical.replace(/\/+$/, '')}/${rest.join('/')}` : canonical;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const isInside = (candidate, root, nodePath) => (
|
|
125
|
+
candidate === root || candidate.startsWith(root.endsWith(nodePath.sep) ? root : `${root}${nodePath.sep}`)
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const hasDotDot = (value) => value.split('/').some((segment) => segment === '..');
|
|
129
|
+
|
|
130
|
+
export const resolveGuestFilePath = async ({
|
|
131
|
+
path: rawPath,
|
|
132
|
+
projectDirectory,
|
|
133
|
+
patterns,
|
|
134
|
+
homeDir,
|
|
135
|
+
fsPromises = defaultFs,
|
|
136
|
+
nodePath = defaultPath,
|
|
137
|
+
}) => {
|
|
138
|
+
if (
|
|
139
|
+
rawPath.length === 0
|
|
140
|
+
|| rawPath.length > GUEST_FILE_PATH_MAX
|
|
141
|
+
|| rawPath.includes('\0')
|
|
142
|
+
|| rawPath.includes('\\')
|
|
143
|
+
|| hasDotDot(rawPath)
|
|
144
|
+
) {
|
|
145
|
+
return failure('BAD_PATH', 'File path is malformed or leaves its scope.');
|
|
146
|
+
}
|
|
147
|
+
if (guestFileScope(rawPath) === 'project') {
|
|
148
|
+
if (!projectDirectory) {
|
|
149
|
+
return failure('NO_DIRECTORY', 'No project is open.');
|
|
150
|
+
}
|
|
151
|
+
let projectReal;
|
|
152
|
+
try {
|
|
153
|
+
projectReal = await fsPromises.realpath(projectDirectory);
|
|
154
|
+
} catch {
|
|
155
|
+
return failure('NO_DIRECTORY', 'The open project directory is not available.');
|
|
156
|
+
}
|
|
157
|
+
const absolute = await canonicalize(nodePath.resolve(projectReal, rawPath), fsPromises, nodePath);
|
|
158
|
+
if (!isInside(absolute, projectReal, nodePath)) {
|
|
159
|
+
return failure('BAD_PATH', 'File path leaves the open project.');
|
|
160
|
+
}
|
|
161
|
+
return { ok: true, scope: 'project', absolute };
|
|
162
|
+
}
|
|
163
|
+
const absolute = await canonicalize(nodePath.resolve(expandHome(rawPath, homeDir)), fsPromises, nodePath);
|
|
164
|
+
const canonicalPatterns = await Promise.all(
|
|
165
|
+
(patterns ?? []).map((pattern) => canonicalizePattern(pattern, homeDir, fsPromises, nodePath)),
|
|
166
|
+
);
|
|
167
|
+
if (!matchesFilesystemPattern(absolute, canonicalPatterns, homeDir, nodePath)) {
|
|
168
|
+
return failure('BAD_PATH', 'File path is not covered by a declared filesystem pattern.');
|
|
169
|
+
}
|
|
170
|
+
return { ok: true, scope: 'filesystem', absolute };
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const entryKind = (dirent) => {
|
|
174
|
+
if (dirent.isSymbolicLink()) return 'other';
|
|
175
|
+
if (dirent.isFile()) return 'file';
|
|
176
|
+
if (dirent.isDirectory()) return 'directory';
|
|
177
|
+
return 'other';
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const readOperation = async (absolute, fsPromises) => {
|
|
181
|
+
const stat = await fsPromises.stat(absolute);
|
|
182
|
+
if (stat.isDirectory()) {
|
|
183
|
+
return failure('BAD_PATH', 'That path is a directory.');
|
|
184
|
+
}
|
|
185
|
+
if (stat.size > GUEST_FILE_CONTENT_MAX) {
|
|
186
|
+
return failure('FILE_TOO_LARGE', `File is over ${GUEST_FILE_CONTENT_MAX} characters.`);
|
|
187
|
+
}
|
|
188
|
+
const content = await fsPromises.readFile(absolute, 'utf8');
|
|
189
|
+
if (content.length > GUEST_FILE_CONTENT_MAX) {
|
|
190
|
+
return failure('FILE_TOO_LARGE', `File is over ${GUEST_FILE_CONTENT_MAX} characters.`);
|
|
191
|
+
}
|
|
192
|
+
return { ok: true, result: { content } };
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const writeOperation = async (absolute, content, fsPromises, nodePath) => {
|
|
196
|
+
if (content === undefined || content === null) {
|
|
197
|
+
return failure('BAD_PATH', 'Write needs content.');
|
|
198
|
+
}
|
|
199
|
+
if (content.length > GUEST_FILE_CONTENT_MAX) {
|
|
200
|
+
return failure('FILE_TOO_LARGE', `Content is over ${GUEST_FILE_CONTENT_MAX} characters.`);
|
|
201
|
+
}
|
|
202
|
+
await fsPromises.mkdir(nodePath.dirname(absolute), { recursive: true });
|
|
203
|
+
// Temp file plus rename: a concurrent reader never sees a truncated file.
|
|
204
|
+
const tmp = `${absolute}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
205
|
+
try {
|
|
206
|
+
await fsPromises.writeFile(tmp, content, 'utf8');
|
|
207
|
+
await fsPromises.rename(tmp, absolute);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
await fsPromises.unlink(tmp).catch(() => {});
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
return { ok: true, result: { written: true } };
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const listOperation = async (absolute, fsPromises) => {
|
|
216
|
+
const dirents = await fsPromises.readdir(absolute, { withFileTypes: true });
|
|
217
|
+
const entries = dirents
|
|
218
|
+
.map((dirent) => ({ name: dirent.name, kind: entryKind(dirent) }))
|
|
219
|
+
.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0))
|
|
220
|
+
.slice(0, GUEST_FILE_LIST_MAX);
|
|
221
|
+
return { ok: true, result: { entries } };
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const statOperation = async (absolute, fsPromises) => {
|
|
225
|
+
let stat;
|
|
226
|
+
try {
|
|
227
|
+
stat = await fsPromises.stat(absolute);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') {
|
|
230
|
+
return { ok: true, result: { kind: 'missing', size: 0, mtime: 0 } };
|
|
231
|
+
}
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
const kind = stat.isFile() ? 'file' : stat.isDirectory() ? 'directory' : 'other';
|
|
235
|
+
return { ok: true, result: { kind, size: stat.size, mtime: Math.floor(stat.mtimeMs) } };
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const mapFsError = (error) => {
|
|
239
|
+
const code = error?.code;
|
|
240
|
+
if (code === 'ENOENT') {
|
|
241
|
+
return failure('NOT_FOUND', 'No file at that path.');
|
|
242
|
+
}
|
|
243
|
+
if (code === 'EACCES' || code === 'EPERM') {
|
|
244
|
+
return failure('DENIED', 'The operating system refused access to that path.');
|
|
245
|
+
}
|
|
246
|
+
if (code === 'EISDIR' || code === 'ENOTDIR') {
|
|
247
|
+
return failure('BAD_PATH', 'That path is not the right kind of entry for this operation.');
|
|
248
|
+
}
|
|
249
|
+
return null;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* One guest file call end to end: scope, grant, then the operation. Returns
|
|
254
|
+
* `{ ok: true, result }` or `{ ok: false, code, message }`; only unexpected
|
|
255
|
+
* OS errors throw.
|
|
256
|
+
*/
|
|
257
|
+
export const runGuestFileOperation = async ({
|
|
258
|
+
op,
|
|
259
|
+
path: rawPath,
|
|
260
|
+
content,
|
|
261
|
+
projectDirectory,
|
|
262
|
+
patterns,
|
|
263
|
+
grants,
|
|
264
|
+
homeDir,
|
|
265
|
+
fsPromises = defaultFs,
|
|
266
|
+
nodePath = defaultPath,
|
|
267
|
+
}) => {
|
|
268
|
+
// Grant first, from the raw path's scope, so an unapproved extension never
|
|
269
|
+
// touches the disk (not even a realpath probe).
|
|
270
|
+
const needed = guestFileScope(rawPath) === 'project' ? 'files' : 'filesystem';
|
|
271
|
+
if (!(grants ?? []).includes(needed)) {
|
|
272
|
+
return failure(
|
|
273
|
+
'NOT_GRANTED',
|
|
274
|
+
needed === 'files'
|
|
275
|
+
? 'This extension has not been allowed to read and write project files.'
|
|
276
|
+
: 'This extension has not been allowed to read and write files outside the project.',
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
let resolved;
|
|
280
|
+
try {
|
|
281
|
+
resolved = await resolveGuestFilePath({ path: rawPath, projectDirectory, patterns, homeDir, fsPromises, nodePath });
|
|
282
|
+
} catch (error) {
|
|
283
|
+
const mapped = mapFsError(error);
|
|
284
|
+
if (mapped) return mapped;
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
if (!resolved.ok) {
|
|
288
|
+
return resolved;
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
switch (op) {
|
|
292
|
+
case 'read':
|
|
293
|
+
return await readOperation(resolved.absolute, fsPromises);
|
|
294
|
+
case 'write':
|
|
295
|
+
return await writeOperation(resolved.absolute, content, fsPromises, nodePath);
|
|
296
|
+
case 'list':
|
|
297
|
+
return await listOperation(resolved.absolute, fsPromises);
|
|
298
|
+
case 'stat':
|
|
299
|
+
return await statOperation(resolved.absolute, fsPromises);
|
|
300
|
+
default:
|
|
301
|
+
return failure('BAD_PATH', 'Unknown file operation.');
|
|
302
|
+
}
|
|
303
|
+
} catch (error) {
|
|
304
|
+
const mapped = mapFsError(error);
|
|
305
|
+
if (mapped) return mapped;
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
308
|
+
};
|