@vantran-se/openchamber-web 1.23.1 → 1.24.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.test.js +29 -6
- package/bin/lib/cli-http.js +5 -1
- package/bin/lib/commands-status.js +5 -0
- package/dist/assets/{CodeMirrorCommentWidgets-BOpqXpFU.js → CodeMirrorCommentWidgets-BjJHrzmU.js} +1 -1
- package/dist/assets/DiffView-COVQhWGd.js +4 -0
- package/dist/assets/FilesView-B1V5JxFX.js +13 -0
- package/dist/assets/GitView-CDOJy5Ay.js +6 -0
- package/dist/assets/GuestAttachDialog-D3S0Rjic.js +1 -0
- package/dist/assets/{JsonTreeView-nd_E8eVM.js → JsonTreeView-D0C9nU7K.js} +1 -1
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/LinearIssuesView-D7FKg1XJ.js +1 -0
- package/dist/assets/{MarkdownImageGallery-CVYds3sQ.js → MarkdownImageGallery-BrFjAVEf.js} +1 -1
- package/dist/assets/MarkdownRendererImpl-DWepTafh.js +12 -0
- package/dist/assets/{OnboardingScreen-Dd2IbPTn.js → OnboardingScreen-Dyxxiqdt.js} +1 -1
- package/dist/assets/{PatchDiff-nUu5i4bG.js → PatchDiff-DnEhjXcC.js} +1 -1
- package/dist/assets/PierreDiffViewer-DyxZVVVR.js +210 -0
- package/dist/assets/PlanView-LW2z_EAP.js +5 -0
- package/dist/assets/PluginPane-B4tKno2h.js +3 -0
- package/dist/assets/SettingsView-DEaMulN9.js +29 -0
- package/dist/assets/{SettingsWindow-BccxY81J.js → SettingsWindow-FLDJXOgQ.js} +1 -1
- package/dist/assets/SubmoduleDiffSummary-CvVwD-Vi.js +1 -0
- package/dist/assets/ToolOutputDialog-BiP0x2Rw.js +15 -0
- package/dist/assets/{ToolPartDiffPreview-BFVgk0UK.js → ToolPartDiffPreview-CkR4msKP.js} +2 -2
- package/dist/assets/{VirtualizedFileDiff-z3_H74UR.js → VirtualizedFileDiff-0pyr_mHY.js} +3 -3
- package/dist/assets/{WalkthroughView-BqM9GFbJ.js → WalkthroughView-yNj4nOP5.js} +2 -2
- package/dist/assets/appThemeRegistry-BuUKhbYQ.js +1 -0
- package/dist/assets/commitAndPush-DvJKa3Ks.js +1 -0
- package/dist/assets/{computeVirtualFileMetrics-Bdazp9Qo.js → computeVirtualFileMetrics-DxTBMuu-.js} +1 -1
- package/dist/assets/de-DgxgFyGG.js +10 -0
- package/dist/assets/es-BFsZj5F_.js +16 -0
- package/dist/assets/fr-jdZ5KKQi.js +14 -0
- package/dist/assets/{index-eo1-D_7z.js → index-CKHjPKbq.js} +1 -1
- package/dist/assets/index-Cbci4i1W.js +40 -0
- package/dist/assets/index-DqaMWNrC.css +1 -0
- package/dist/assets/{isDiffPlainText-BvKH712-.js → isDiffPlainText-D7FkBMP6.js} +1 -1
- package/dist/assets/{isFilePlainText-BdVVPukf.js → isFilePlainText-CL2vsrIZ.js} +1 -1
- package/dist/assets/ja-C5kKrhO4.js +16 -0
- package/dist/assets/ko-DXDTT_0O.js +16 -0
- package/dist/assets/main-CZALtVek.js +896 -0
- package/dist/assets/main-CgXMBNvG.js +2 -0
- package/dist/assets/{markdown-shiki.worker-D5fBU-lw.js → markdown-shiki.worker-CahLvHIq.js} +5 -5
- package/dist/assets/markdownCore-Dh7WEsdM.js +5 -0
- package/dist/assets/miniChat-CwTmDixi.js +2 -0
- package/dist/assets/mobile-CLf2vCtO.js +2 -0
- package/dist/assets/{patchFileDiff-B7LjppSS.js → patchFileDiff-Bsba8KFp.js} +3 -3
- package/dist/assets/pl-D-7x4Y8Q.js +16 -0
- package/dist/assets/pt-BR-CSBZrHa8.js +16 -0
- package/dist/assets/renderElectronMiniChatApp-C5gAFBk0.js +2 -0
- package/dist/assets/renderMobileApp-V4hYd32H.js +25 -0
- package/dist/assets/runtimeEndpointReset-DhlHCn-2.js +25 -0
- package/dist/assets/{scheduledTasksApi-hceovDei.js → scheduledTasksApi-DzjbLd8P.js} +1 -1
- package/dist/assets/shikiHighlight-8_3csmr6.js +1 -0
- package/dist/assets/textMateThemeFromAppTheme-CHo2fCgW.js +1 -0
- package/dist/assets/tr-Dr-oJ22L.js +7 -0
- package/dist/assets/uk-9Nx83UcI.js +16 -0
- package/dist/assets/useAppFontEffects-khZXpFL_.js +7333 -0
- package/dist/assets/useInlineCommentController-DkQ_gqpn.js +2 -0
- package/dist/assets/useRootScrollLock-DdvHf9Pt.js +2 -0
- package/dist/assets/{vendor-base-ui-BY_5qla_.js → vendor-base-ui-CsXYnR5c.js} +1 -1
- package/dist/assets/{vendor-codemirror-language-data-DWA1bm1F.js → vendor-codemirror-language-data-CdzUmUyZ.js} +2 -2
- package/dist/assets/vendor-framer-motion-Bq3iTZ6r.js +1 -0
- package/dist/assets/vendor-katex-Cp579UoJ.css +1 -0
- package/dist/assets/{vendor-motion-dom-D_o3BTI3.js → vendor-motion-dom-DcCgeG69.js} +1 -1
- package/dist/assets/{vendor-remixicon-react-Dil0zKKT.js → vendor-remixicon-react-BDmGmFCA.js} +1 -1
- package/dist/assets/vendor-strip-json-comments-5dJlOkP1.js +4 -0
- package/dist/assets/{vendor-tanstack-react-virtual-g05Cx3z9.js → vendor-tanstack-react-virtual-ClYN8CzB.js} +1 -1
- package/dist/assets/{vendor-tanstack-virtual-core-Dubg10g5.js → vendor-tanstack-virtual-core-BewVQryB.js} +1 -1
- package/dist/assets/{vendor-zod-B6551dwG.js → vendor-zod-CgAdXWZO.js} +4 -4
- package/dist/assets/{web-components-B_Z64So5.js → web-components-B8IcVQAA.js} +2 -2
- package/dist/assets/zh-CN-Bxy_Hj3r.js +16 -0
- package/dist/assets/zh-TW-0BB4gpHM.js +16 -0
- package/dist/index.html +6 -4
- package/dist/mini-chat.html +6 -4
- package/dist/mobile.html +6 -4
- package/package.json +12 -9
- package/server/built-in-extensions/registry.json +4 -0
- package/server/index.d.ts +2 -0
- package/server/index.js +44 -10
- package/server/lib/agent-tool/DOCUMENTATION.md +17 -4
- package/server/lib/agent-tool/runtime.js +62 -15
- package/server/lib/agent-tool/runtime.test.js +88 -2
- package/server/lib/changelog/update-notes.js +2 -1
- package/server/lib/changelog/update-notes.test.js +19 -0
- package/server/lib/event-stream/DOCUMENTATION.md +23 -0
- package/server/lib/event-stream/delta-coalescer.js +0 -0
- package/server/lib/event-stream/delta-coalescer.test.js +292 -0
- package/server/lib/event-stream/global-hub.js +53 -23
- package/server/lib/event-stream/global-hub.test.js +160 -0
- package/server/lib/event-stream/global-ws-bridge.js +1 -0
- package/server/lib/event-stream/index.js +4 -0
- package/server/lib/fs/DOCUMENTATION.md +10 -1
- package/server/lib/fs/git-process.test.js +77 -0
- package/server/lib/fs/routes.js +58 -5
- package/server/lib/fs/routes.test.js +116 -0
- package/server/lib/fs/search.js +2 -1
- package/server/lib/git/DOCUMENTATION.md +20 -5
- package/server/lib/git/routes.js +16 -3
- package/server/lib/git/serial-refresh.js +93 -0
- package/server/lib/git/serial-refresh.test.js +112 -0
- package/server/lib/git/service.js +429 -70
- package/server/lib/git/service.test.js +418 -0
- package/server/lib/github/pr-status.js +5 -5
- package/server/lib/github/pr-status.test.js +1 -1
- package/server/lib/github/routes.js +4 -4
- package/server/lib/guests/DOCUMENTATION.md +68 -0
- package/server/lib/guests/auth-store.js +184 -0
- package/server/lib/guests/auth-store.test.js +112 -0
- package/server/lib/guests/background.test.js +83 -0
- package/server/lib/guests/builtins.js +34 -0
- package/server/lib/guests/builtins.test.js +174 -0
- package/server/lib/guests/catalog.js +477 -0
- package/server/lib/guests/catalog.test.js +390 -0
- package/server/lib/guests/clone.js +297 -0
- package/server/lib/guests/extract-zip.js +136 -0
- package/server/lib/guests/extract-zip.test.js +109 -0
- package/server/lib/guests/files.js +308 -0
- package/server/lib/guests/files.test.js +262 -0
- package/server/lib/guests/grant-scope.js +81 -0
- package/server/lib/guests/grant-scope.test.js +75 -0
- package/server/lib/guests/host-session.js +73 -0
- package/server/lib/guests/host-session.test.js +30 -0
- package/server/lib/guests/html-styles.js +6 -0
- package/server/lib/guests/html-styles.test.js +45 -0
- package/server/lib/guests/html-tokens.js +73 -0
- package/server/lib/guests/html-tokens.test.js +53 -0
- package/server/lib/guests/install.js +406 -0
- package/server/lib/guests/install.test.js +338 -0
- package/server/lib/guests/oauth.js +501 -0
- package/server/lib/guests/oauth.test.js +481 -0
- package/server/lib/guests/persist.js +336 -0
- package/server/lib/guests/persist.test.js +288 -0
- package/server/lib/guests/request.js +112 -0
- package/server/lib/guests/request.test.js +206 -0
- package/server/lib/guests/routes.js +838 -0
- package/server/lib/guests/service.js +538 -0
- package/server/lib/guests/service.test.js +277 -0
- package/server/lib/guests/sockets.js +129 -0
- package/server/lib/guests/sockets.test.js +110 -0
- package/server/lib/guests/ssh-install.test.js +188 -0
- package/server/lib/guests/storage.js +68 -0
- package/server/lib/guests/storage.test.js +59 -0
- package/server/lib/guests/updates.js +294 -0
- package/server/lib/guests/updates.test.js +334 -0
- package/server/lib/guests/upload.js +50 -0
- package/server/lib/guests/upload.test.js +131 -0
- package/server/lib/linear/DOCUMENTATION.md +1 -1
- package/server/lib/message-queue/DOCUMENTATION.md +10 -1
- package/server/lib/message-queue/runtime.js +21 -1
- package/server/lib/message-queue/runtime.test.js +52 -5
- package/server/lib/notifications/routes.js +5 -0
- package/server/lib/openchamber-control/DOCUMENTATION.md +6 -0
- package/server/lib/openchamber-sessions/routes.js +62 -29
- package/server/lib/openchamber-sessions/routes.test.js +96 -0
- package/server/lib/opencode/DOCUMENTATION.md +55 -1
- package/server/lib/opencode/bootstrap-runtime.js +1 -0
- package/server/lib/opencode/core-routes.js +39 -1
- package/server/lib/opencode/feature-routes-runtime.js +15 -0
- package/server/lib/opencode/lifecycle-process.test.js +84 -0
- package/server/lib/opencode/lifecycle.js +54 -67
- package/server/lib/opencode/openchamber-routes.js +72 -14
- package/server/lib/opencode/openchamber-routes.test.js +71 -3
- package/server/lib/opencode/session-runtime.js +68 -0
- package/server/lib/opencode/session-runtime.test.js +43 -0
- package/server/lib/opencode/settings-helpers.js +8 -0
- package/server/lib/opencode/settings-helpers.test.js +13 -1
- package/server/lib/opencode/settings-normalization-runtime.js +2 -0
- package/server/lib/opencode/settings-normalization-runtime.test.js +13 -0
- package/server/lib/opencode/settings-registry.json +15 -0
- package/server/lib/opencode/shutdown-runtime.js +3 -0
- package/server/lib/opencode/shutdown-runtime.test.js +24 -0
- package/server/lib/opencode/theme-archive.js +53 -0
- package/server/lib/opencode/theme-catalog.js +171 -0
- package/server/lib/opencode/theme-catalog.test.js +125 -0
- package/server/lib/opencode/theme-import.test.js +144 -0
- package/server/lib/opencode/theme-runtime.js +100 -28
- package/server/lib/opencode/theme-runtime.test.js +15 -0
- package/server/lib/permission-auto-accept/DOCUMENTATION.md +8 -0
- package/server/lib/permission-auto-accept/runtime.js +13 -0
- package/server/lib/permission-auto-accept/runtime.test.js +37 -1
- package/server/lib/projects/project-config.js +2 -2
- package/server/lib/quota/DOCUMENTATION.md +0 -1
- package/server/lib/quota/providers/index.js +0 -7
- package/server/lib/realtime-proxy-terminal.test.js +150 -0
- package/server/lib/realtime-proxy.js +2 -0
- package/server/lib/routing/DOCUMENTATION.md +83 -0
- package/server/lib/routing/defaults.js +98 -0
- package/server/lib/routing/feature-flag.js +11 -0
- package/server/lib/routing/history.js +34 -0
- package/server/lib/routing/jev.js +98 -0
- package/server/lib/routing/routes.http.test.js +100 -0
- package/server/lib/routing/routes.js +81 -0
- package/server/lib/routing/runtime.js +217 -0
- package/server/lib/routing/runtime.test.js +188 -0
- package/server/lib/routing/store.js +249 -0
- package/server/lib/routing/store.test.js +96 -0
- package/server/lib/scheduled-tasks/runtime.js +2 -2
- package/server/lib/scheduled-tasks/runtime.test.js +14 -0
- package/server/lib/security/request-security.js +9 -5
- package/server/lib/security/request-security.test.js +37 -0
- package/server/lib/small-model/DOCUMENTATION.md +14 -2
- package/server/lib/small-model/call.js +37 -6
- package/server/lib/small-model/call.test.js +182 -0
- package/server/lib/terminal/DOCUMENTATION.md +23 -2
- package/server/lib/terminal/runtime.js +27 -5
- package/server/lib/terminal/runtime.test.js +67 -0
- package/server/lib/terminal/shutdown.js +106 -0
- package/server/lib/terminal/shutdown.test.js +84 -0
- package/server/lib/tts/service.test.js +78 -0
- package/server/lib/ui-auth/DOCUMENTATION.md +8 -0
- package/server/lib/ui-auth/session-cookie.js +51 -0
- package/server/lib/ui-auth/session-cookie.test.js +46 -0
- package/server/lib/ui-auth/ui-auth.js +56 -17
- package/server/lib/ui-auth/ui-auth.test.js +96 -0
- package/dist/assets/DiffView-CyLmb-wF.js +0 -4
- package/dist/assets/DirtyBranchSwitchDialog-pu5kcoir.js +0 -1
- package/dist/assets/FilesView-WtJB0y_l.js +0 -13
- package/dist/assets/GitView-DkNrV0BK.js +0 -6
- package/dist/assets/LinearIssuesView-B0H0Otii.js +0 -1
- package/dist/assets/MarkdownRendererImpl-DSqVN2d8.js +0 -12
- package/dist/assets/PierreDiffViewer-Bsq9z-sw.js +0 -161
- package/dist/assets/PlanView-CAFziKH5.js +0 -5
- package/dist/assets/SettingsView-BRittavZ.js +0 -28
- package/dist/assets/ToolOutputDialog-B0uYaJio.js +0 -15
- package/dist/assets/appThemeRegistry-BK0dHKa6.js +0 -1
- package/dist/assets/de-c7CYLCAW.js +0 -10
- package/dist/assets/es-BKAVPQmQ.js +0 -16
- package/dist/assets/fr-Bj1glqfV.js +0 -14
- package/dist/assets/index-BYFyOu_D.css +0 -1
- package/dist/assets/index-BfwRClDe.js +0 -40
- package/dist/assets/ja-5jwSIbK0.js +0 -16
- package/dist/assets/ko-BaDbg-Eb.js +0 -16
- package/dist/assets/main-CGG2_uHq.js +0 -2
- package/dist/assets/main-CcrSaL4J.js +0 -930
- package/dist/assets/markdownCore-DsmQr3bt.js +0 -5
- package/dist/assets/miniChat-DEaAhP4b.js +0 -2
- package/dist/assets/mobile-BC3rsvv0.js +0 -2
- package/dist/assets/pl-BFKVBDFu.js +0 -16
- package/dist/assets/pt-BR-qKfHvkDL.js +0 -16
- package/dist/assets/registerCustomTheme-BBXUCRnG.js +0 -1
- package/dist/assets/renderElectronMiniChatApp-CR9d1Mr0.js +0 -2
- package/dist/assets/renderMobileApp-Da5dwobZ.js +0 -25
- package/dist/assets/runtimeEndpointReset-CLYVRw2H.js +0 -41
- package/dist/assets/shikiHighlight-D1Z9G58W.js +0 -1
- package/dist/assets/tr-DYXPoEDG.js +0 -7
- package/dist/assets/uk-CwAoloR8.js +0 -16
- package/dist/assets/useAppFontEffects-DeStGcUT.js +0 -7282
- package/dist/assets/useGitComparison-CpxioduU.js +0 -1
- package/dist/assets/useInlineCommentController-gmUnJtrb.js +0 -2
- package/dist/assets/useRootScrollLock--a3cPz8s.js +0 -1
- package/dist/assets/vendor-framer-motion-livSKyyV.js +0 -1
- package/dist/assets/zh-CN-D9MG0lqF.js +0 -16
- package/dist/assets/zh-TW-1HNK7shh.js +0 -16
- package/server/lib/quota/providers/crof.js +0 -96
- package/server/lib/quota/providers/crof.test.js +0 -98
- /package/dist/assets/{vendor-katex-k3Ppq1Ha.js → vendor-katex-CHD9BdDO.js} +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
2
|
|
|
3
3
|
import { isAgentMemoryFeatureAvailable } from '../agent-memory/feature-flag.js';
|
|
4
|
+
import { isRoutingFeatureAvailable } from '../routing/feature-flag.js';
|
|
4
5
|
|
|
5
6
|
// Generated from packages/ui/src/lib/settings/registry.ts by
|
|
6
7
|
// `bun run settings-registry:generate`; `registry.test.ts` fails when stale.
|
|
@@ -233,6 +234,11 @@ export const createSettingsHelpers = (dependencies) => {
|
|
|
233
234
|
if (typeof candidate.workStatusHiddenSectionsExplicit === 'boolean') {
|
|
234
235
|
result.workStatusHiddenSectionsExplicit = candidate.workStatusHiddenSectionsExplicit;
|
|
235
236
|
}
|
|
237
|
+
if (Array.isArray(candidate.workStatusSectionOrder)) {
|
|
238
|
+
result.workStatusSectionOrder = [
|
|
239
|
+
...new Set(candidate.workStatusSectionOrder.filter((entry) => typeof entry === 'string' && entry.length > 0)),
|
|
240
|
+
];
|
|
241
|
+
}
|
|
236
242
|
if (typeof candidate.desktopLanAccessEnabled === 'boolean') {
|
|
237
243
|
result.desktopLanAccessEnabled = candidate.desktopLanAccessEnabled;
|
|
238
244
|
}
|
|
@@ -1008,6 +1014,8 @@ export const createSettingsHelpers = (dependencies) => {
|
|
|
1008
1014
|
// Tells the client whether agent memory exists in this build at all, so
|
|
1009
1015
|
// its settings row and panel tab can be absent rather than merely off.
|
|
1010
1016
|
agentMemoryFeatureAvailable: isAgentMemoryFeatureAvailable(),
|
|
1017
|
+
// Same idea for Jev routing: absent from the picker and Settings unless the build has it.
|
|
1018
|
+
routingFeatureAvailable: isRoutingFeatureAvailable(),
|
|
1011
1019
|
...(pwaAppName ? { pwaAppName } : {}),
|
|
1012
1020
|
pwaOrientation,
|
|
1013
1021
|
mobileKeyboardMode,
|
|
@@ -69,6 +69,18 @@ const createTestHelpersWithRealSanitizers = () => {
|
|
|
69
69
|
};
|
|
70
70
|
|
|
71
71
|
describe('settings helpers', () => {
|
|
72
|
+
it('round-trips section order and preserves it across unrelated writes', () => {
|
|
73
|
+
const helpers = createTestHelpers();
|
|
74
|
+
const changes = helpers.sanitizeSettingsUpdate({ workStatusSectionOrder: ['mcp', 'session', 'mcp', null, ''] });
|
|
75
|
+
expect(changes.workStatusSectionOrder).toEqual(['mcp', 'session']);
|
|
76
|
+
const saved = helpers.mergePersistedSettings({}, changes);
|
|
77
|
+
const reloaded = helpers.formatSettingsResponse(JSON.parse(JSON.stringify(saved)));
|
|
78
|
+
expect(reloaded.workStatusSectionOrder).toEqual(['mcp', 'session']);
|
|
79
|
+
const next = helpers.mergePersistedSettings(reloaded, helpers.sanitizeSettingsUpdate({ workStatusPanelEnabled: false }));
|
|
80
|
+
expect(helpers.formatSettingsResponse(next).workStatusSectionOrder).toEqual(['mcp', 'session']);
|
|
81
|
+
expect(helpers.sanitizeSettingsUpdate({ workStatusSectionOrder: 'bad' }).workStatusSectionOrder).toBeUndefined();
|
|
82
|
+
expect(helpers.sanitizeSettingsUpdate({ workStatusSectionOrder: [] }).workStatusSectionOrder).toEqual([]);
|
|
83
|
+
});
|
|
72
84
|
it('round-trips telemetry opt-in with the hidden list and preserves it across unrelated writes', () => {
|
|
73
85
|
const helpers = createTestHelpers();
|
|
74
86
|
const legacy = helpers.sanitizeSettingsUpdate({ workStatusHiddenSections: [] });
|
|
@@ -762,7 +774,7 @@ describe('settings registry gate', () => {
|
|
|
762
774
|
managedRemoteTunnelHostname: 'x.example', managedRemoteTunnelToken: 'token', managedRemoteTunnelPresets: [{ id: 'a', name: 'A', hostname: 'a.example' }],
|
|
763
775
|
managedRemoteTunnelSelectedPresetId: 'a', managedRemoteTunnelPresetTokens: { a: 'token' },
|
|
764
776
|
sidebarProjectDisplayMode: 'all', sidebarSessionGroupingMode: 'flat', sidebarProjectSortOrder: 'manual', sidebarShowRecentSection: true,
|
|
765
|
-
workStatusPanelEnabled: true, workStatusHiddenSections: ['mcp'], workStatusHiddenSectionsExplicit: true,
|
|
777
|
+
workStatusPanelEnabled: true, workStatusHiddenSections: ['mcp'], workStatusHiddenSectionsExplicit: true, workStatusSectionOrder: ['mcp', 'session'],
|
|
766
778
|
showReasoningTraces: true, streamingAutoFollowEnabled: true, collapsibleThinkingBlocks: true, showTextJustificationActivity: true,
|
|
767
779
|
chatRenderMode: 'live', activityRenderMode: 'summary', mermaidRenderingMode: 'svg', userMessageRenderingMode: 'markdown', collapsibleUserMessages: true,
|
|
768
780
|
stickyUserHeader: true, promptNavigatorEnabled: true, wideChatLayoutEnabled: true, showSplitAssistantMessageActions: true, showToolFileIcons: true,
|
|
@@ -154,6 +154,7 @@ export const createSettingsNormalizationRuntime = (dependencies) => {
|
|
|
154
154
|
: null;
|
|
155
155
|
const iconBackground = normalizeIconBackground(candidate.iconBackground);
|
|
156
156
|
const color = typeof candidate.color === 'string' ? candidate.color.trim() : '';
|
|
157
|
+
const defaultAgent = typeof candidate.defaultAgent === 'string' ? candidate.defaultAgent.trim() : '';
|
|
157
158
|
const defaultModel = typeof candidate.defaultModel === 'string' ? candidate.defaultModel.trim() : '';
|
|
158
159
|
const defaultVariant = typeof candidate.defaultVariant === 'string' ? candidate.defaultVariant.trim() : '';
|
|
159
160
|
const addedAt = Number.isFinite(candidate.addedAt) ? Number(candidate.addedAt) : null;
|
|
@@ -175,6 +176,7 @@ export const createSettingsNormalizationRuntime = (dependencies) => {
|
|
|
175
176
|
...(icon ? { icon } : {}),
|
|
176
177
|
...(iconBackground ? { iconBackground } : {}),
|
|
177
178
|
...(color ? { color } : {}),
|
|
179
|
+
...(defaultAgent ? { defaultAgent } : {}),
|
|
178
180
|
...(defaultModel && defaultModel.includes('/') ? { defaultModel } : {}),
|
|
179
181
|
// A variant is meaningless without the model it belongs to.
|
|
180
182
|
...(defaultModel && defaultModel.includes('/') && defaultVariant ? { defaultVariant } : {}),
|
|
@@ -122,6 +122,19 @@ describe('settings normalization runtime - symlink resolution', () => {
|
|
|
122
122
|
expect(result[1].defaultVariant).toBe(undefined);
|
|
123
123
|
});
|
|
124
124
|
|
|
125
|
+
it('keeps a project default agent', () => {
|
|
126
|
+
const runtime = createTestRuntime({
|
|
127
|
+
realpathSync: (p) => p,
|
|
128
|
+
path: { resolve: (p) => p, sep: '/', dirname: (p) => p.split('/').slice(0, -1).join('/') || '/' },
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const result = runtime.sanitizeProjects([
|
|
132
|
+
{ id: 'proj1', path: '/a', defaultAgent: 'plan' },
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
expect(result[0].defaultAgent).toBe('plan');
|
|
136
|
+
});
|
|
137
|
+
|
|
125
138
|
it('deduplicates projects that resolve to the same realpath', () => {
|
|
126
139
|
const runtime = createTestRuntime({
|
|
127
140
|
realpathSync: (p) => p.startsWith('/symlink') ? '/real/project' : p,
|
|
@@ -127,6 +127,10 @@
|
|
|
127
127
|
"scope": "instance",
|
|
128
128
|
"computed": true
|
|
129
129
|
},
|
|
130
|
+
"routingFeatureAvailable": {
|
|
131
|
+
"scope": "instance",
|
|
132
|
+
"computed": true
|
|
133
|
+
},
|
|
130
134
|
"openCodeUpdateToastDismissedVersion": {
|
|
131
135
|
"scope": "instance"
|
|
132
136
|
},
|
|
@@ -217,6 +221,9 @@
|
|
|
217
221
|
"sidebarShowRecentSection": {
|
|
218
222
|
"scope": "profile"
|
|
219
223
|
},
|
|
224
|
+
"workStatusSectionOrder": {
|
|
225
|
+
"scope": "profile"
|
|
226
|
+
},
|
|
220
227
|
"workStatusPanelEnabled": {
|
|
221
228
|
"scope": "profile"
|
|
222
229
|
},
|
|
@@ -605,6 +612,10 @@
|
|
|
605
612
|
"scope": "device",
|
|
606
613
|
"local": true
|
|
607
614
|
},
|
|
615
|
+
"contextEditorVisible": {
|
|
616
|
+
"scope": "device",
|
|
617
|
+
"local": true
|
|
618
|
+
},
|
|
608
619
|
"contextEditorTreeWidth": {
|
|
609
620
|
"scope": "device",
|
|
610
621
|
"local": true
|
|
@@ -617,6 +628,10 @@
|
|
|
617
628
|
"scope": "device",
|
|
618
629
|
"local": true
|
|
619
630
|
},
|
|
631
|
+
"messageQueueExpanded": {
|
|
632
|
+
"scope": "device",
|
|
633
|
+
"local": true
|
|
634
|
+
},
|
|
620
635
|
"workStatusScrollTop": {
|
|
621
636
|
"scope": "device",
|
|
622
637
|
"local": true
|
|
@@ -108,6 +108,9 @@ export const createGracefulShutdownRuntime = (dependencies) => {
|
|
|
108
108
|
console.log('HTTP server closed');
|
|
109
109
|
resolve();
|
|
110
110
|
});
|
|
111
|
+
// The backend has stopped. Active SSE/HTTP clients must not keep
|
|
112
|
+
// Desktop waiting for the outer shutdown deadline.
|
|
113
|
+
server.closeAllConnections?.();
|
|
111
114
|
}),
|
|
112
115
|
new Promise((resolve) => {
|
|
113
116
|
closeTimeout = setTimeout(() => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import http from 'node:http';
|
|
2
3
|
|
|
3
4
|
import { createGracefulShutdownRuntime } from './shutdown-runtime.js';
|
|
4
5
|
|
|
@@ -55,4 +56,27 @@ describe('graceful shutdown runtime', () => {
|
|
|
55
56
|
expect(warnSpy).not.toHaveBeenCalledWith('Server close timeout reached, forcing shutdown');
|
|
56
57
|
expect(vi.getTimerCount()).toBe(0);
|
|
57
58
|
});
|
|
59
|
+
|
|
60
|
+
it('closes an active HTTP stream instead of waiting for the shutdown deadline', async () => {
|
|
61
|
+
const server = http.createServer((_request, response) => {
|
|
62
|
+
response.writeHead(200, { 'content-type': 'text/event-stream' });
|
|
63
|
+
response.write('data: fixture\n\n');
|
|
64
|
+
});
|
|
65
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
66
|
+
const request = http.get(`http://127.0.0.1:${server.address().port}`);
|
|
67
|
+
request.on('error', () => {});
|
|
68
|
+
const response = await new Promise((resolve) => request.once('response', resolve));
|
|
69
|
+
response.resume();
|
|
70
|
+
const closed = new Promise((resolve) => response.once('close', resolve));
|
|
71
|
+
const warning = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
72
|
+
try {
|
|
73
|
+
await createRuntime(server).gracefulShutdown({ exitProcess: false });
|
|
74
|
+
expect(warning).not.toHaveBeenCalledWith('Server close timeout reached, forcing shutdown');
|
|
75
|
+
await closed;
|
|
76
|
+
} finally {
|
|
77
|
+
request.destroy();
|
|
78
|
+
server.closeAllConnections();
|
|
79
|
+
await new Promise((resolve) => server.close(resolve));
|
|
80
|
+
}
|
|
81
|
+
});
|
|
58
82
|
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { inflateRaw } from 'node:zlib';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
|
|
4
|
+
const inflate = promisify(inflateRaw);
|
|
5
|
+
const MAX_FILE_BYTES = 512 * 1024;
|
|
6
|
+
|
|
7
|
+
// Read only selected JSON entries in memory. Nothing from a VSIX is executed or
|
|
8
|
+
// extracted onto the host filesystem.
|
|
9
|
+
export function openThemeArchive(bytes) {
|
|
10
|
+
let end = bytes.length - 22;
|
|
11
|
+
const lower = Math.max(0, end - 65535);
|
|
12
|
+
while (end >= lower && (bytes.readUInt32LE(end) !== 0x06054b50 || end + 22 + bytes.readUInt16LE(end + 20) !== bytes.length)) end--;
|
|
13
|
+
if (end < lower) throw new Error('Invalid VSIX directory');
|
|
14
|
+
const count = bytes.readUInt16LE(end + 10);
|
|
15
|
+
const directorySize = bytes.readUInt32LE(end + 12);
|
|
16
|
+
let cursor = bytes.readUInt32LE(end + 16);
|
|
17
|
+
if (count > 5000 || bytes.readUInt16LE(end + 4) || bytes.readUInt16LE(end + 6)
|
|
18
|
+
|| bytes.readUInt16LE(end + 8) !== count || cursor + directorySize !== end) throw new Error('Unsupported VSIX directory');
|
|
19
|
+
const entries = new Map();
|
|
20
|
+
let totalSize = 0;
|
|
21
|
+
for (let index = 0; index < count; index++) {
|
|
22
|
+
if (cursor + 46 > end || bytes.readUInt32LE(cursor) !== 0x02014b50) throw new Error('Invalid VSIX entry');
|
|
23
|
+
const flags = bytes.readUInt16LE(cursor + 8);
|
|
24
|
+
const method = bytes.readUInt16LE(cursor + 10);
|
|
25
|
+
const compressed = bytes.readUInt32LE(cursor + 20);
|
|
26
|
+
const size = bytes.readUInt32LE(cursor + 24);
|
|
27
|
+
const nameLength = bytes.readUInt16LE(cursor + 28);
|
|
28
|
+
const next = cursor + 46 + nameLength + bytes.readUInt16LE(cursor + 30) + bytes.readUInt16LE(cursor + 32);
|
|
29
|
+
const offset = bytes.readUInt32LE(cursor + 42);
|
|
30
|
+
if (next > end || nameLength > 1024 || flags & 1 || ![0, 8].includes(method)) throw new Error('Unsupported VSIX entry');
|
|
31
|
+
const name = bytes.toString('utf8', cursor + 46, cursor + 46 + nameLength);
|
|
32
|
+
const parts = name.replace(/\/$/, '').split('/');
|
|
33
|
+
if (parts.some((part) => !part || part === '.' || part === '..') || /[\\\0:]/.test(name) || entries.has(name)) throw new Error('Unsafe VSIX path');
|
|
34
|
+
totalSize += size;
|
|
35
|
+
if (totalSize > 100 * 1024 * 1024) throw new Error('VSIX contents too large');
|
|
36
|
+
entries.set(name, { offset, compressed, size, method });
|
|
37
|
+
cursor = next;
|
|
38
|
+
}
|
|
39
|
+
if (cursor !== end) throw new Error('Invalid VSIX directory size');
|
|
40
|
+
return async (name) => {
|
|
41
|
+
const entry = entries.get(name);
|
|
42
|
+
if (!entry || entry.size > MAX_FILE_BYTES) throw new Error('Theme file missing or too large');
|
|
43
|
+
const { offset, compressed, size, method } = entry;
|
|
44
|
+
if (offset + 30 > bytes.length || bytes.readUInt32LE(offset) !== 0x04034b50 || bytes.readUInt16LE(offset + 8) !== method) throw new Error('Invalid VSIX file');
|
|
45
|
+
const nameLength = bytes.readUInt16LE(offset + 26);
|
|
46
|
+
const start = offset + 30 + nameLength + bytes.readUInt16LE(offset + 28);
|
|
47
|
+
if (start + compressed > bytes.length || bytes.toString('utf8', offset + 30, offset + 30 + nameLength) !== name) throw new Error('Invalid VSIX file bounds');
|
|
48
|
+
const data = bytes.subarray(start, start + compressed);
|
|
49
|
+
const result = method === 0 ? data : await inflate(data, { maxOutputLength: MAX_FILE_BYTES });
|
|
50
|
+
if (result.length !== size) throw new Error('Invalid VSIX file size');
|
|
51
|
+
return result.toString('utf8');
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { parse } from 'jsonc-parser';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { openThemeArchive } from './theme-archive.js';
|
|
6
|
+
|
|
7
|
+
const identifier = z.string().min(1).max(160).regex(/^[\w-]+$/);
|
|
8
|
+
const version = z.string().min(1).max(80).regex(/^[\w.+-]+$/);
|
|
9
|
+
const settings = z.object({ foreground: z.string().optional() });
|
|
10
|
+
const tokenRules = z.array(z.object({ scope: z.union([z.string(), z.array(z.string())]).optional(), settings }));
|
|
11
|
+
const sourceSchema = z.object({
|
|
12
|
+
name: z.string().optional(), type: z.string().optional(), include: z.string().optional(),
|
|
13
|
+
colors: z.record(z.string(), z.string().nullable()).default({}),
|
|
14
|
+
tokenColors: z.union([z.string(), tokenRules]).optional(),
|
|
15
|
+
semanticHighlighting: z.boolean().optional(),
|
|
16
|
+
semanticTokenColors: z.record(z.string(), z.union([z.string(), settings])).optional(),
|
|
17
|
+
});
|
|
18
|
+
const extensionSchema = z.object({
|
|
19
|
+
namespace: identifier, name: identifier, version,
|
|
20
|
+
displayName: z.string().optional(), files: z.object({ icon: z.string().optional() }),
|
|
21
|
+
});
|
|
22
|
+
const detailSchema = extensionSchema.extend({ files: z.object({ download: z.string(), sha256: z.string() }) });
|
|
23
|
+
const manifestSchema = z.object({
|
|
24
|
+
publisher: identifier, name: identifier, version,
|
|
25
|
+
contributes: z.object({ themes: z.array(z.object({
|
|
26
|
+
label: z.string().min(1).max(160), path: z.string().min(1).max(1024),
|
|
27
|
+
uiTheme: z.enum(['vs', 'vs-dark', 'hc-black', 'hc-light']).optional(),
|
|
28
|
+
})).min(1).max(40) }),
|
|
29
|
+
});
|
|
30
|
+
const querySchema = z.object({ query: z.string().trim().min(1).max(160) });
|
|
31
|
+
const packageSchema = z.object({ namespace: identifier, name: identifier, version });
|
|
32
|
+
const hosts = new Set(['open-vsx.org', 'openvsx.eclipsecontent.org']);
|
|
33
|
+
|
|
34
|
+
function trustedUrl(value) {
|
|
35
|
+
const url = new URL(value);
|
|
36
|
+
if (url.protocol !== 'https:' || !hosts.has(url.hostname) || url.port || url.username || url.password) throw new Error('Unexpected catalog host');
|
|
37
|
+
return url;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function download(url, maxBytes, signal, fetchImpl) {
|
|
41
|
+
for (let redirects = 0; redirects < 4; redirects++) {
|
|
42
|
+
const response = await fetchImpl(trustedUrl(url), { signal, redirect: 'manual' });
|
|
43
|
+
if ([301, 302, 303, 307, 308].includes(response.status)) {
|
|
44
|
+
await response.body?.cancel();
|
|
45
|
+
url = new URL(response.headers.get('location') ?? '', url).href;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (!response.ok || !response.body || Number(response.headers.get('content-length')) > maxBytes) {
|
|
49
|
+
await response.body?.cancel();
|
|
50
|
+
throw new Error('Catalog request failed');
|
|
51
|
+
}
|
|
52
|
+
const reader = response.body.getReader();
|
|
53
|
+
const chunks = [];
|
|
54
|
+
let length = 0;
|
|
55
|
+
try {
|
|
56
|
+
while (true) {
|
|
57
|
+
const { done, value } = await reader.read();
|
|
58
|
+
if (done) break;
|
|
59
|
+
length += value.byteLength;
|
|
60
|
+
if (length > maxBytes) throw new Error('Catalog response too large');
|
|
61
|
+
chunks.push(value);
|
|
62
|
+
}
|
|
63
|
+
} finally {
|
|
64
|
+
await reader.cancel();
|
|
65
|
+
reader.releaseLock();
|
|
66
|
+
}
|
|
67
|
+
return Buffer.concat(chunks, length);
|
|
68
|
+
}
|
|
69
|
+
throw new Error('Too many catalog redirects');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parseJsonc(text, schema) {
|
|
73
|
+
const errors = [];
|
|
74
|
+
const value = parse(text, errors, { allowTrailingComma: true });
|
|
75
|
+
if (errors.length) throw new Error('Invalid theme JSON');
|
|
76
|
+
return schema.parse(value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function themePath(from, reference) {
|
|
80
|
+
if (/[\\\0:]/.test(reference) || reference.startsWith('/')) throw new Error('Invalid theme reference');
|
|
81
|
+
const resolved = path.posix.normalize(path.posix.join(path.posix.dirname(from), reference));
|
|
82
|
+
if (!resolved.startsWith('extension/')) throw new Error('Theme reference escapes package');
|
|
83
|
+
return resolved;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function createThemeCatalog({ fetchImpl = fetch } = {}) {
|
|
87
|
+
const search = async (input, signal = AbortSignal.timeout(15000)) => {
|
|
88
|
+
const { query } = querySchema.parse(input);
|
|
89
|
+
const url = new URL('https://open-vsx.org/api/-/search');
|
|
90
|
+
url.search = new URLSearchParams({ query, category: 'Themes', size: '24', sortBy: 'relevance' }).toString();
|
|
91
|
+
const bytes = await download(url.href, 1024 * 1024, signal, fetchImpl);
|
|
92
|
+
const result = z.object({ extensions: z.array(extensionSchema).max(24) }).parse(JSON.parse(bytes.toString('utf8')));
|
|
93
|
+
return result.extensions.map((extension) => ({
|
|
94
|
+
namespace: extension.namespace, name: extension.name, version: extension.version,
|
|
95
|
+
label: extension.displayName || extension.name,
|
|
96
|
+
icon: extension.files.icon && URL.canParse(extension.files.icon) && hosts.has(new URL(extension.files.icon).hostname)
|
|
97
|
+
? trustedUrl(extension.files.icon).href : null,
|
|
98
|
+
}));
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const readPackage = async (input, signal = AbortSignal.timeout(45000)) => {
|
|
102
|
+
const requested = packageSchema.parse(input);
|
|
103
|
+
const url = `https://open-vsx.org/api/${requested.namespace}/${requested.name}/${requested.version}`;
|
|
104
|
+
const detail = detailSchema.parse(JSON.parse((await download(url, 512 * 1024, signal, fetchImpl)).toString('utf8')));
|
|
105
|
+
const bytes = await download(detail.files.download, 20 * 1024 * 1024, signal, fetchImpl);
|
|
106
|
+
const checksum = (await download(detail.files.sha256, 256, signal, fetchImpl)).toString('utf8').trim().split(/\s+/)[0];
|
|
107
|
+
if (createHash('sha256').update(bytes).digest('hex') !== checksum?.toLowerCase()) throw new Error('VSIX checksum mismatch');
|
|
108
|
+
const read = openThemeArchive(bytes);
|
|
109
|
+
const manifest = parseJsonc(await read('extension/package.json'), manifestSchema);
|
|
110
|
+
if (manifest.publisher.toLowerCase() !== requested.namespace.toLowerCase() || manifest.name !== requested.name || manifest.version !== requested.version) throw new Error('VSIX identity mismatch');
|
|
111
|
+
let reads = 0;
|
|
112
|
+
const load = async (file, ancestors = []) => {
|
|
113
|
+
signal.throwIfAborted();
|
|
114
|
+
if (ancestors.includes(file) || ancestors.length >= 8 || ++reads > 320) throw new Error('Too many theme references');
|
|
115
|
+
const source = parseJsonc(await read(file), sourceSchema);
|
|
116
|
+
const parent = source.include ? await load(themePath(file, source.include), [...ancestors, file]) : null;
|
|
117
|
+
const reference = z.string().safeParse(source.tokenColors);
|
|
118
|
+
const rules = reference.success
|
|
119
|
+
? parseJsonc(await read(themePath(file, reference.data)), tokenRules) : tokenRules.parse(source.tokenColors ?? []);
|
|
120
|
+
const { include: _include, ...own } = source;
|
|
121
|
+
return {
|
|
122
|
+
...parent, ...own,
|
|
123
|
+
colors: { ...parent?.colors, ...source.colors },
|
|
124
|
+
tokenColors: [...(parent?.tokenColors ?? []), ...(rules ?? [])],
|
|
125
|
+
semanticTokenColors: { ...parent?.semanticTokenColors, ...source.semanticTokenColors },
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
const themes = [];
|
|
129
|
+
let totalBytes = 0;
|
|
130
|
+
for (const contribution of manifest.contributes.themes) {
|
|
131
|
+
try {
|
|
132
|
+
const file = themePath('extension/package.json', contribution.path);
|
|
133
|
+
const source = await load(file);
|
|
134
|
+
source.name = contribution.label;
|
|
135
|
+
if (contribution.uiTheme) source.type = contribution.uiTheme === 'vs' ? 'light' : contribution.uiTheme === 'vs-dark' ? 'dark' : contribution.uiTheme;
|
|
136
|
+
const text = JSON.stringify(source);
|
|
137
|
+
totalBytes += Buffer.byteLength(text);
|
|
138
|
+
if (Buffer.byteLength(text) > 512 * 1024 || totalBytes > 10 * 1024 * 1024) throw new Error('Resolved themes too large');
|
|
139
|
+
themes.push({ path: contribution.path, name: contribution.label, text, error: false });
|
|
140
|
+
} catch {
|
|
141
|
+
signal.throwIfAborted();
|
|
142
|
+
themes.push({ path: contribution.path, name: contribution.label, text: '', error: true });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return themes;
|
|
146
|
+
};
|
|
147
|
+
return { search, readPackage };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function registerThemeCatalogRoutes(app, catalog = createThemeCatalog()) {
|
|
151
|
+
for (const [route, operation, schema] of [
|
|
152
|
+
['/api/config/themes/catalog/search', catalog.search, querySchema],
|
|
153
|
+
['/api/config/themes/catalog/package', catalog.readPackage, packageSchema],
|
|
154
|
+
]) {
|
|
155
|
+
app.post(route, async (req, res) => {
|
|
156
|
+
const input = schema.safeParse(req.body);
|
|
157
|
+
if (!input.success) return res.status(400).json({ error: 'invalid' });
|
|
158
|
+
const controller = new AbortController();
|
|
159
|
+
const cancel = () => controller.abort();
|
|
160
|
+
res.on('close', cancel);
|
|
161
|
+
try {
|
|
162
|
+
const items = await operation(input.data, AbortSignal.any([controller.signal, AbortSignal.timeout(45000)]));
|
|
163
|
+
res.json({ items });
|
|
164
|
+
} catch {
|
|
165
|
+
if (!res.destroyed) res.status(502).json({ error: 'catalog' });
|
|
166
|
+
} finally {
|
|
167
|
+
res.off('close', cancel);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { deflateRawSync } from 'node:zlib';
|
|
4
|
+
import { createThemeCatalog, registerThemeCatalogRoutes } from './theme-catalog.js';
|
|
5
|
+
import { registerCommonRequestMiddleware } from './core-routes.js';
|
|
6
|
+
import express from 'express';
|
|
7
|
+
import request from 'supertest';
|
|
8
|
+
import { openThemeArchive } from './theme-archive.js';
|
|
9
|
+
|
|
10
|
+
function archive(files) {
|
|
11
|
+
const locals = [];
|
|
12
|
+
const directory = [];
|
|
13
|
+
let offset = 0;
|
|
14
|
+
for (const [filename, content] of Object.entries(files)) {
|
|
15
|
+
const name = Buffer.from(filename);
|
|
16
|
+
const data = Buffer.from(content);
|
|
17
|
+
const compressed = deflateRawSync(data);
|
|
18
|
+
const header = Buffer.alloc(30);
|
|
19
|
+
header.writeUInt32LE(0x04034b50);
|
|
20
|
+
header.writeUInt16LE(8, 8);
|
|
21
|
+
header.writeUInt32LE(compressed.length, 18);
|
|
22
|
+
header.writeUInt32LE(data.length, 22);
|
|
23
|
+
header.writeUInt16LE(name.length, 26);
|
|
24
|
+
locals.push(header, name, compressed);
|
|
25
|
+
const central = Buffer.alloc(46);
|
|
26
|
+
central.writeUInt32LE(0x02014b50);
|
|
27
|
+
central.writeUInt16LE(8, 10);
|
|
28
|
+
central.writeUInt32LE(compressed.length, 20);
|
|
29
|
+
central.writeUInt32LE(data.length, 24);
|
|
30
|
+
central.writeUInt16LE(name.length, 28);
|
|
31
|
+
central.writeUInt32LE(offset, 42);
|
|
32
|
+
directory.push(central, name);
|
|
33
|
+
offset += header.length + name.length + compressed.length;
|
|
34
|
+
}
|
|
35
|
+
const end = Buffer.alloc(22);
|
|
36
|
+
end.writeUInt32LE(0x06054b50);
|
|
37
|
+
end.writeUInt16LE(directory.length / 2, 8);
|
|
38
|
+
end.writeUInt16LE(directory.length / 2, 10);
|
|
39
|
+
end.writeUInt32LE(Buffer.concat(directory).length, 12);
|
|
40
|
+
end.writeUInt32LE(offset, 16);
|
|
41
|
+
return Buffer.concat([...locals, ...directory, end]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const identity = { namespace: 'Author', name: 'fixture', version: '1.0.0' };
|
|
45
|
+
const manifest = { publisher: 'Author', name: 'fixture', version: '1.0.0', contributes: { themes: [
|
|
46
|
+
{ label: 'Readable Night', path: './themes/night.json', uiTheme: 'vs-dark' },
|
|
47
|
+
{ label: 'Broken sibling', path: '../../outside.json', uiTheme: 'vs' },
|
|
48
|
+
] } };
|
|
49
|
+
const packageFiles = {
|
|
50
|
+
'extension/package.json': JSON.stringify(manifest),
|
|
51
|
+
'extension/base.json': '{"colors":{"editor.background":"#111111","editor.foreground":"#eeeeee"},"tokenColors":[{"scope":"string","settings":{"foreground":"#aaaaaa"}}]}',
|
|
52
|
+
'extension/themes/night.json': '{ // JSONC\n"include":"../base.json", "colors":{"editor.foreground":"#ffffff"},"tokenColors":"./tokens.json",}',
|
|
53
|
+
'extension/themes/tokens.json': '[{"scope":"keyword","settings":{"foreground":"#aabbcc"}}]',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function transport(bytes, overrides = {}) {
|
|
57
|
+
const responses = new Map([
|
|
58
|
+
['https://open-vsx.org/api/Author/fixture/1.0.0', JSON.stringify({ ...identity, files: { download: 'https://open-vsx.org/package.vsix', sha256: 'https://open-vsx.org/package.sha256' } })],
|
|
59
|
+
['https://open-vsx.org/package.vsix', bytes],
|
|
60
|
+
['https://open-vsx.org/package.sha256', createHash('sha256').update(bytes).digest('hex')],
|
|
61
|
+
...Object.entries(overrides),
|
|
62
|
+
]);
|
|
63
|
+
return async (url) => {
|
|
64
|
+
const body = responses.get(url.href);
|
|
65
|
+
if (body === undefined) throw new Error(`Unexpected request ${url}`);
|
|
66
|
+
return new Response(body);
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe('Open VSX theme catalog', () => {
|
|
71
|
+
test('catalog requests retain their bodies through the production middleware', async () => {
|
|
72
|
+
const app = express();
|
|
73
|
+
registerCommonRequestMiddleware(app, { express });
|
|
74
|
+
const catalog = createThemeCatalog({ fetchImpl: transport(archive(packageFiles)) });
|
|
75
|
+
registerThemeCatalogRoutes(app, {
|
|
76
|
+
search: async (input) => { expect(input).toEqual({ query: 'nord' }); return []; },
|
|
77
|
+
readPackage: catalog.readPackage,
|
|
78
|
+
});
|
|
79
|
+
await request(app).post('/api/config/themes/catalog/search').send({ query: 'nord' }).expect(200);
|
|
80
|
+
const response = await request(app).post('/api/config/themes/catalog/package').send(identity).expect(200);
|
|
81
|
+
expect(response.body.items[0].name).toBe('Readable Night');
|
|
82
|
+
});
|
|
83
|
+
test('resolves JSONC includes and external token files, preserving healthy siblings and manifest labels', async () => {
|
|
84
|
+
const catalog = createThemeCatalog({ fetchImpl: transport(archive(packageFiles)) });
|
|
85
|
+
const result = await catalog.readPackage(identity);
|
|
86
|
+
expect(result[0].error).toBe(false);
|
|
87
|
+
const source = JSON.parse(result[0].text);
|
|
88
|
+
expect(source.name).toBe('Readable Night');
|
|
89
|
+
expect(source.type).toBe('dark');
|
|
90
|
+
expect(source.colors).toEqual({ 'editor.background': '#111111', 'editor.foreground': '#ffffff' });
|
|
91
|
+
expect(source.tokenColors.map((rule) => rule.scope)).toEqual(['string', 'keyword']);
|
|
92
|
+
expect(source.include).toBeUndefined();
|
|
93
|
+
expect(result[1].error).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('rejects checksum and package identity mismatches', async () => {
|
|
97
|
+
const bytes = archive(packageFiles);
|
|
98
|
+
await expect(createThemeCatalog({ fetchImpl: transport(bytes, { 'https://open-vsx.org/package.sha256': '0'.repeat(64) }) }).readPackage(identity)).rejects.toThrow('checksum');
|
|
99
|
+
const wrong = archive({ ...packageFiles, 'extension/package.json': JSON.stringify({ ...manifest, publisher: 'Impostor' }) });
|
|
100
|
+
await expect(createThemeCatalog({ fetchImpl: transport(wrong) }).readPackage(identity)).rejects.toThrow('identity');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('limits decompression and rejects traversal and cyclic includes', async () => {
|
|
104
|
+
expect(() => openThemeArchive(archive({ '../outside.json': '{}' }))).toThrow('Unsafe');
|
|
105
|
+
const read = openThemeArchive(archive({ 'extension/huge.json': ' '.repeat(512 * 1024 + 1) }));
|
|
106
|
+
await expect(read('extension/huge.json')).rejects.toThrow('large');
|
|
107
|
+
const cyclic = archive({ ...packageFiles, 'extension/base.json': '{"include":"./themes/night.json"}' });
|
|
108
|
+
expect((await createThemeCatalog({ fetchImpl: transport(cyclic) }).readPackage(identity))[0].error).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('searches only the theme category and rejects redirects outside the catalog', async () => {
|
|
112
|
+
const catalog = createThemeCatalog({ fetchImpl: async (url, options) => {
|
|
113
|
+
expect(url.searchParams.get('category')).toBe('Themes');
|
|
114
|
+
expect(url.searchParams.get('query')).toBe('Dune');
|
|
115
|
+
expect(options.redirect).toBe('manual');
|
|
116
|
+
return Response.json({ extensions: [{ ...identity, displayName: 'Dune', files: {} }] });
|
|
117
|
+
} });
|
|
118
|
+
expect((await catalog.search({ query: 'Dune' }))[0].label).toBe('Dune');
|
|
119
|
+
let calls = 0;
|
|
120
|
+
const hostile = createThemeCatalog({ fetchImpl: async () => { calls++; return new Response(null, { status: 302, headers: { location: 'http://127.0.0.1/private' } }); } });
|
|
121
|
+
await expect(hostile.search({ query: 'Dune' })).rejects.toThrow('host');
|
|
122
|
+
expect(calls).toBe(1);
|
|
123
|
+
await expect(catalog.readPackage({ ...identity, name: '../secret' })).rejects.toThrow();
|
|
124
|
+
});
|
|
125
|
+
});
|