@forsion/unit 2.12.2
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/LICENSE +223 -0
- package/README.md +101 -0
- package/backendWorker.mjs +273 -0
- package/main.mjs +3434 -0
- package/package.json +26 -0
- package/release.json +11 -0
- package/web/assets/ArtificialView-B4ESD4iK.js +1 -0
- package/web/assets/AutomationBuilder-sxWdUa0L.js +1 -0
- package/web/assets/CalendarConfigView-cCfo_EvZ.js +1 -0
- package/web/assets/CalendarView-wgLapctk.js +1 -0
- package/web/assets/CapabilityMenu-BQBsM638.js +1 -0
- package/web/assets/CapabilityMenu-CTIzmTJB.css +1 -0
- package/web/assets/ChatView-BIUk0OJd.css +1 -0
- package/web/assets/ChatView-C-jCJnyS.js +47 -0
- package/web/assets/CheckboxInput-BsaoUHZ0.js +1 -0
- package/web/assets/Chrome-CwFiDmre.js +1 -0
- package/web/assets/CodeView-j065KtVh.js +15 -0
- package/web/assets/DancingScript-Bold-latin-CWNX0RqN.woff2 +0 -0
- package/web/assets/DashboardGridView-Ck3LKkOb.css +1 -0
- package/web/assets/DashboardGridView-DXqe0vPd.js +17 -0
- package/web/assets/EventCard-BmDQkEj9.js +1 -0
- package/web/assets/ExcalidrawCanvas-4uYrLYin.css +1 -0
- package/web/assets/ExcalidrawCanvas-C3egkikA.js +1 -0
- package/web/assets/FloatingToc-CENybkAA.js +1 -0
- package/web/assets/HomepageView-BNabJQfP.css +1 -0
- package/web/assets/HomepageView-CRdhjLXu.js +1 -0
- package/web/assets/ImageStudioPanels-Dke7xf8E.js +3 -0
- package/web/assets/ImageStudioView-g38V8gaj.js +1 -0
- package/web/assets/InlineFiles-C_hjvn3u.js +1 -0
- package/web/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/web/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/web/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/web/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/web/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/web/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/web/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/web/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/web/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/web/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/web/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/web/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/web/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/web/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/web/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/web/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/web/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/web/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/web/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/web/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/web/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/web/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/web/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/web/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/web/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/web/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/web/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/web/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/web/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/web/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/web/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/web/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/web/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/web/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/web/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/web/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/web/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/web/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/web/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/web/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/web/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/web/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/web/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/web/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/web/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/web/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/web/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/web/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/web/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/web/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/web/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/web/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/web/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/web/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/web/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/web/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/web/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/web/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/web/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/web/assets/MarkdownBlock-DIjYEhTK.js +56 -0
- package/web/assets/ModelPill-BPlgovPo.js +1 -0
- package/web/assets/ModelPill-CzdNh7Mt.css +1 -0
- package/web/assets/MuseLibraryView-CenyTTim.js +2 -0
- package/web/assets/MuseView-d-iBk3sc.js +6 -0
- package/web/assets/PageHistoryPanel-CR-k1AM7.js +2 -0
- package/web/assets/PageHistoryPanel-DFjh0pWs.css +1 -0
- package/web/assets/PdfAnnotator-CGzC_o1Q.css +1 -0
- package/web/assets/PdfAnnotator-D79ip27w.js +560 -0
- package/web/assets/ProductView-u2aezVsl.js +1 -0
- package/web/assets/RightPanel-C3OJTIqT.js +4 -0
- package/web/assets/RunLocationPicker-BNcZ7KKF.css +1 -0
- package/web/assets/RunLocationPicker-CTYXUH2z.js +34 -0
- package/web/assets/SidebarRow-BuSkKH5q.js +9 -0
- package/web/assets/SidebarRow-CsWEqPKX.css +1 -0
- package/web/assets/SketchCard-D87GADyG.js +437 -0
- package/web/assets/StudioGuestSurface-BijU_EEb.js +1 -0
- package/web/assets/TodoListView-DPPVpPUn.js +1 -0
- package/web/assets/Tooltip-DY4ISA-e.js +1 -0
- package/web/assets/UnifiedPage-CKNtVD_n.js +57 -0
- package/web/assets/UnifiedPage-DX_sTpyZ.css +1 -0
- package/web/assets/amadeus-manual.en-DWaN2fPs.js +3515 -0
- package/web/assets/amadeus-manual.zh--7D5TS8X.js +3515 -0
- package/web/assets/apl-B4CMkyY2.js +1 -0
- package/web/assets/appStore-Dg9WALEA.js +3080 -0
- package/web/assets/appStore-Di8LXuJ9.css +1 -0
- package/web/assets/approvalText-Cy2UAUAQ.js +2 -0
- package/web/assets/arrow-up-right-CpQs92mu.js +1 -0
- package/web/assets/artificial-BzLFkChP.css +1 -0
- package/web/assets/asciiarmor-Df11BRmG.js +1 -0
- package/web/assets/asn1-EdZsLKOL.js +1 -0
- package/web/assets/asterisk-B-8jnY81.js +1 -0
- package/web/assets/astryxBridge-BZhaZPs4.js +70 -0
- package/web/assets/astryxBridge-CF_hpqC2.css +1 -0
- package/web/assets/automationListSource-DP5xZBxo.js +1 -0
- package/web/assets/brainfuck-C4LP7Hcl.js +1 -0
- package/web/assets/calendarNavStore-C7fGRpoH.js +1 -0
- package/web/assets/canvasPrefs-BeG4oEgP.js +1 -0
- package/web/assets/chatBoxSurface-zvcyfrxE.js +1 -0
- package/web/assets/circle-question-mark-WL3vbp-Z.js +1 -0
- package/web/assets/circle-x-D7s-M5sB.js +1 -0
- package/web/assets/client-BSKpsyAw.js +9 -0
- package/web/assets/clike-B9uivgTg.js +1 -0
- package/web/assets/clojure-BMjYHr_A.js +1 -0
- package/web/assets/cmake-BQqOBYOt.js +1 -0
- package/web/assets/cobol-CWcv1MsR.js +1 -0
- package/web/assets/code-xml-BALV9u_c.js +1 -0
- package/web/assets/codeStudioStore-C2mWfm32.js +22 -0
- package/web/assets/coffeescript-S37ZYGWr.js +1 -0
- package/web/assets/commonlisp-DBKNyK5s.js +1 -0
- package/web/assets/create-4xUyKtAF.js +14 -0
- package/web/assets/create-BgE3edmZ.css +1 -0
- package/web/assets/crystal-SjHAIU92.js +1 -0
- package/web/assets/css-BnMrqG3P.js +1 -0
- package/web/assets/cursor-editorTextHighlight-kG3uUOwL.svg +8 -0
- package/web/assets/cypher-C_CwsFkJ.js +1 -0
- package/web/assets/d-pRatUO7H.js +1 -0
- package/web/assets/dashboardSurface-CFTH5z4c.js +1 -0
- package/web/assets/deskCapture-Df701U8L.js +1 -0
- package/web/assets/diff-DbItnlRl.js +1 -0
- package/web/assets/diff2html-BV-zCH4d.js +111 -0
- package/web/assets/dockerfile-BKs6k2Af.js +1 -0
- package/web/assets/docx-preview-DW4IyU_Q.js +22 -0
- package/web/assets/dtd-DF_7sFjM.js +1 -0
- package/web/assets/dylan-DwRh75JA.js +1 -0
- package/web/assets/ebnf-CDyGwa7X.js +1 -0
- package/web/assets/ecl-Cabwm37j.js +1 -0
- package/web/assets/eiffel-CnydiIhH.js +1 -0
- package/web/assets/ellipsis-r1EcXsFq.js +1 -0
- package/web/assets/elm-vLlmbW-K.js +1 -0
- package/web/assets/emoji-DGaxpCIu.js +1 -0
- package/web/assets/erlang-BNw1qcRV.js +1 -0
- package/web/assets/factor-kuTfRLto.js +1 -0
- package/web/assets/fcl-Kvtd6kyn.js +1 -0
- package/web/assets/floatingTocSurface-C6RJ6i41.js +1 -0
- package/web/assets/foldActions-B7KByFdN.js +1 -0
- package/web/assets/folder-DlxnPmWY.js +1 -0
- package/web/assets/folder-plus-D1TDb7k9.js +1 -0
- package/web/assets/forsion-logo-dark-DnB9JTdv.svg +5 -0
- package/web/assets/forsion-logo-light-DhkEeB4V.svg +11 -0
- package/web/assets/forth-Ffai-XNe.js +1 -0
- package/web/assets/fortran-DYz_wnZ1.js +1 -0
- package/web/assets/frame-_xBVYe3P.js +1 -0
- package/web/assets/gas-Bneqetm1.js +1 -0
- package/web/assets/gestures-CkW1Kfs9.js +1 -0
- package/web/assets/gherkin-heZmZLOM.js +1 -0
- package/web/assets/groovy-D9Dt4D0W.js +1 -0
- package/web/assets/hack2gate-dark-Dx2RtDRI.jpg +0 -0
- package/web/assets/hack2gate-light-DRKq9tyS.jpg +0 -0
- package/web/assets/hand-Crrw-nSn.js +1 -0
- package/web/assets/hard-drive-BbyVSbfP.js +1 -0
- package/web/assets/haskell-Cw1EW3IL.js +1 -0
- package/web/assets/haxe-H-WmDvRZ.js +1 -0
- package/web/assets/http-DBlCnlav.js +1 -0
- package/web/assets/i18n-D-eU58uw.css +1 -0
- package/web/assets/i18n.generated-Bbsz16cl.js +15 -0
- package/web/assets/idl-BEugSyMb.js +1 -0
- package/web/assets/imageStudio-BqbNOvpu.css +1 -0
- package/web/assets/imageStudio-C_Rs630L.js +2 -0
- package/web/assets/index--drOLtq7.js +1 -0
- package/web/assets/index-BCZZVJ_w.js +1 -0
- package/web/assets/index-BCubCwdT.js +1 -0
- package/web/assets/index-BRW82CsP.js +6 -0
- package/web/assets/index-BYsdKR_1.js +1 -0
- package/web/assets/index-Brik2pf9.js +1 -0
- package/web/assets/index-C6GJulug.js +13 -0
- package/web/assets/index-C7U6T-k8.js +1 -0
- package/web/assets/index-CF4RV4mh.js +298 -0
- package/web/assets/index-CflzrdSt.js +1 -0
- package/web/assets/index-Cmr7gVZQ.js +1 -0
- package/web/assets/index-CqyM6tQ0.js +1 -0
- package/web/assets/index-CzM-C2Sd.js +1 -0
- package/web/assets/index-Czu_bbDM.js +1 -0
- package/web/assets/index-D1f_JwPH.js +1 -0
- package/web/assets/index-DKhyDkGB.js +7 -0
- package/web/assets/index-DUWR2nZi.js +1 -0
- package/web/assets/index-DZfZTksa.js +1 -0
- package/web/assets/index-Dg9oixs4.js +1 -0
- package/web/assets/index-DjzSBM0U.js +2 -0
- package/web/assets/index-KL5Rj9kP.js +1 -0
- package/web/assets/index-OHi69jDq.js +3 -0
- package/web/assets/index-SZ5waGAq.js +1 -0
- package/web/assets/index-gPSbFSUn.js +1 -0
- package/web/assets/index-tAXBa_fN.js +1 -0
- package/web/assets/invitePage-lLQ-e0pj.js +19 -0
- package/web/assets/ipc-DQOD2UKk.js +1 -0
- package/web/assets/javascript-iXu5QeM3.js +1 -0
- package/web/assets/jszip.min-DZxV9uZ_.js +2 -0
- package/web/assets/julia-DuME0IfC.js +1 -0
- package/web/assets/layout-grid-B_pjJwz4.js +1 -0
- package/web/assets/linkFollow-C0_3k_PC.js +8 -0
- package/web/assets/list-tree-DkAJYEXd.js +1 -0
- package/web/assets/listFold-BsOVn9UZ.js +1 -0
- package/web/assets/livescript-BwQOo05w.js +1 -0
- package/web/assets/lock-BnGHxjeE.js +1 -0
- package/web/assets/log-out-DBuMeqik.js +1 -0
- package/web/assets/lua-VAEuO923.js +1 -0
- package/web/assets/main-Dki8NlsT.js +21 -0
- package/web/assets/main-NgUPPqDn.css +1 -0
- package/web/assets/markdownEditorSurface-B4PeUzed.js +3 -0
- package/web/assets/markdownEditorSurface-Bql20IAf.css +1 -0
- package/web/assets/mathematica-DTrFuWx2.js +1 -0
- package/web/assets/mbox-CNhZ1qSd.js +1 -0
- package/web/assets/mdMarkStore-CsKl7C8O.js +2 -0
- package/web/assets/mdMarks-Bq7Hl-4C.js +1 -0
- package/web/assets/message-square-plus-BiAZAvcw.js +1 -0
- package/web/assets/messages-BH5MfQdZ.js +1 -0
- package/web/assets/minus-Cyv1YzOU.js +1 -0
- package/web/assets/mirc-CjQqDB4T.js +1 -0
- package/web/assets/mllike-CXdrOF99.js +1 -0
- package/web/assets/mobileEntry-Bd_nKr0D.js +13 -0
- package/web/assets/mobileEntry-DS1vohuL.css +1 -0
- package/web/assets/modelica-Dc1JOy9r.js +1 -0
- package/web/assets/mscgen-BA5vi2Kp.js +1 -0
- package/web/assets/mumps-BT43cFF4.js +1 -0
- package/web/assets/nativeDownload-D-f1_L2S.js +1 -0
- package/web/assets/nginx-DdIZxoE0.js +1 -0
- package/web/assets/nsis-LdVXkNf5.js +1 -0
- package/web/assets/ntriples-BfvgReVJ.js +1 -0
- package/web/assets/octave-Ck1zUtKM.js +1 -0
- package/web/assets/oz-BzwKVEFT.js +1 -0
- package/web/assets/pageChrome-C9_azUYW.js +1 -0
- package/web/assets/pageChrome-xLNdFaSg.css +1 -0
- package/web/assets/pageTree-CZH2J6cc.js +7 -0
- package/web/assets/panel-left-rOc8As0s.js +1 -0
- package/web/assets/pascal--L3eBynH.js +1 -0
- package/web/assets/pdf-C2_sJa3b.js +57 -0
- package/web/assets/pdf.worker.min-CGb3BKV9.js +1 -0
- package/web/assets/pdf.worker.min-Dr1KORA9.mjs +31 -0
- package/web/assets/pen-line-m6_HvWZ9.js +1 -0
- package/web/assets/pen-tool-lkmBqliH.js +1 -0
- package/web/assets/pencil-Cvk91P_Z.js +1 -0
- package/web/assets/perl-CdXCOZ3F.js +1 -0
- package/web/assets/pig-CevX1Tat.js +1 -0
- package/web/assets/pluginExt-Byb64tQO.js +1 -0
- package/web/assets/powershell-CFHJl5sT.js +1 -0
- package/web/assets/properties-C78fOPTZ.js +1 -0
- package/web/assets/protobuf-ChK-085T.js +1 -0
- package/web/assets/pug-DeIclll2.js +1 -0
- package/web/assets/puppet-DMA9R1ak.js +1 -0
- package/web/assets/python-BuPzkPfP.js +1 -0
- package/web/assets/q-pXgVlZs6.js +1 -0
- package/web/assets/r-B6wPVr8A.js +1 -0
- package/web/assets/redo-2-DSi54z4e.js +1 -0
- package/web/assets/rotate-ccw-CUlnplr0.js +1 -0
- package/web/assets/rpm-CTu-6PCP.js +1 -0
- package/web/assets/ruby-B2Rjki9n.js +1 -0
- package/web/assets/sas-B4kiWyti.js +1 -0
- package/web/assets/scheme-C41bIUwD.js +1 -0
- package/web/assets/sharePage-MM6UpPJ9.js +2 -0
- package/web/assets/shareViewer-CDZ8lTdy.js +345 -0
- package/web/assets/shareViewer-DLVrwanO.css +1 -0
- package/web/assets/shell-CjFT_Tl9.js +1 -0
- package/web/assets/sieve-C3Gn_uJK.js +1 -0
- package/web/assets/simple-mode-GW_nhZxv.js +1 -0
- package/web/assets/smalltalk-CnHTOXQT.js +1 -0
- package/web/assets/solr-DehyRSwq.js +1 -0
- package/web/assets/sparql-DkYu6x3z.js +1 -0
- package/web/assets/spreadsheet-BCZA_wO0.js +1 -0
- package/web/assets/sql-D0XecflT.js +1 -0
- package/web/assets/star-CwDiGyUW.js +1 -0
- package/web/assets/stex-C3f8Ysf7.js +1 -0
- package/web/assets/sticky-note-BIuPUSOU.js +1 -0
- package/web/assets/stylus-B533Al4x.js +1 -0
- package/web/assets/swift-BzpIVaGY.js +1 -0
- package/web/assets/tableSurface-CDNa0QFp.css +1 -0
- package/web/assets/tableSurface-Y_JBQsuz.js +1 -0
- package/web/assets/tcl-DVfN8rqt.js +1 -0
- package/web/assets/terminal-DIXogYJY.js +1 -0
- package/web/assets/terminalView-B3aZSsrQ.js +12 -0
- package/web/assets/terminalView-DFuMZ0ql.css +1 -0
- package/web/assets/textile-CnDTJFAw.js +1 -0
- package/web/assets/theme-DfEi_DND.css +1 -0
- package/web/assets/theme-OMpAfNto.css +1 -0
- package/web/assets/tiddlywiki-DO-Gjzrf.js +1 -0
- package/web/assets/tiki-DGYXhP31.js +1 -0
- package/web/assets/toml-Bm5Em-hy.js +1 -0
- package/web/assets/troff-wAsdV37c.js +1 -0
- package/web/assets/ttcn-CfJYG6tj.js +1 -0
- package/web/assets/ttcn-cfg-B9xdYoR4.js +1 -0
- package/web/assets/turtle-B1tBg_DP.js +1 -0
- package/web/assets/type-Cqoabh7H.js +1 -0
- package/web/assets/undo-2-tUKCv5Gs.js +1 -0
- package/web/assets/unitAccount-D8lVwaaT.js +1 -0
- package/web/assets/unitBridge-D547ublk.js +1 -0
- package/web/assets/unitCloudTransport-BVF6lVp4.js +1 -0
- package/web/assets/unitOwnerAccess-BfnEjsZb.js +1 -0
- package/web/assets/unitShim-DxetRgmP.js +5 -0
- package/web/assets/upload-BbeAsMuD.js +1 -0
- package/web/assets/userSpaces-DyyAn7vj.js +1 -0
- package/web/assets/vb-CmGdzxic.js +1 -0
- package/web/assets/vbscript-BuJXcnF6.js +1 -0
- package/web/assets/velocity-D8B20fx6.js +1 -0
- package/web/assets/verilog-C6RDOZhf.js +1 -0
- package/web/assets/vhdl-lSbBsy5d.js +1 -0
- package/web/assets/viewportLock-C4SX-9PO.js +103 -0
- package/web/assets/viewportLock-DYSe7Jsn.css +1 -0
- package/web/assets/waves-horizontal-B8ZpHQ5H.js +1 -0
- package/web/assets/webidl-ZXfAyPTL.js +1 -0
- package/web/assets/workflow-DuoVRSSX.js +1 -0
- package/web/assets/xlsx-CNerDvZX.js +105 -0
- package/web/assets/xquery-DzFWVndE.js +1 -0
- package/web/assets/yacas-BJ4BC0dw.js +1 -0
- package/web/assets/z80-Hz9HOZM7.js +1 -0
- package/web/assets/zoom-in-hr5bRpDL.js +1 -0
- package/web/excalidraw/excalidraw.css +1 -0
- package/web/excalidraw/excalidraw.js +579 -0
- package/web/excalidraw/fonts/Assistant/Assistant-Bold.woff2 +0 -0
- package/web/excalidraw/fonts/Assistant/Assistant-Medium.woff2 +0 -0
- package/web/excalidraw/fonts/Assistant/Assistant-Regular.woff2 +0 -0
- package/web/excalidraw/fonts/Assistant/Assistant-SemiBold.woff2 +0 -0
- package/web/excalidraw/fonts/Cascadia/CascadiaCode-Regular.woff2 +0 -0
- package/web/excalidraw/fonts/ComicShanns/ComicShanns-Regular-279a7b317d12eb88de06167bd672b4b4.woff2 +0 -0
- package/web/excalidraw/fonts/ComicShanns/ComicShanns-Regular-6e066e8de2ac57ea9283adb9c24d7f0c.woff2 +0 -0
- package/web/excalidraw/fonts/ComicShanns/ComicShanns-Regular-dc6a8806fa96795d7b3be5026f989a17.woff2 +0 -0
- package/web/excalidraw/fonts/ComicShanns/ComicShanns-Regular-fcb0fc02dcbee4c9846b3e2508668039.woff2 +0 -0
- package/web/excalidraw/fonts/Excalifont/Excalifont-Regular-349fac6ca4700ffec595a7150a0d1e1d.woff2 +0 -0
- package/web/excalidraw/fonts/Excalifont/Excalifont-Regular-3f2c5db56cc93c5a6873b1361d730c16.woff2 +0 -0
- package/web/excalidraw/fonts/Excalifont/Excalifont-Regular-41b173a47b57366892116a575a43e2b6.woff2 +0 -0
- package/web/excalidraw/fonts/Excalifont/Excalifont-Regular-623ccf21b21ef6b3a0d87738f77eb071.woff2 +0 -0
- package/web/excalidraw/fonts/Excalifont/Excalifont-Regular-a88b72a24fb54c9f94e3b5fdaa7481c9.woff2 +0 -0
- package/web/excalidraw/fonts/Excalifont/Excalifont-Regular-b9dcf9d2e50a1eaf42fc664b50a3fd0d.woff2 +0 -0
- package/web/excalidraw/fonts/Excalifont/Excalifont-Regular-be310b9bcd4f1a43f571c46df7809174.woff2 +0 -0
- package/web/excalidraw/fonts/Liberation/LiberationSans-Regular.woff2 +0 -0
- package/web/excalidraw/fonts/Lilita/Lilita-Regular-i7dPIFZ9Zz-WBtRtedDbYE98RXi4EwSsbg.woff2 +0 -0
- package/web/excalidraw/fonts/Lilita/Lilita-Regular-i7dPIFZ9Zz-WBtRtedDbYEF8RXi4EwQ.woff2 +0 -0
- package/web/excalidraw/fonts/Nunito/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTA3j6zbXWjgevT5.woff2 +0 -0
- package/web/excalidraw/fonts/Nunito/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2 +0 -0
- package/web/excalidraw/fonts/Nunito/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTk3j6zbXWjgevT5.woff2 +0 -0
- package/web/excalidraw/fonts/Nunito/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTo3j6zbXWjgevT5.woff2 +0 -0
- package/web/excalidraw/fonts/Nunito/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTs3j6zbXWjgevT5.woff2 +0 -0
- package/web/excalidraw/fonts/Virgil/Virgil-Regular.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-019d66dcad46dc156b162d267f981c20.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-04b718e5623574919c8b0dea5f301444.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-069e77aac84590e2e991d0a0176d34f2.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-06c77b8c66e51ed6c63ccb502dd8b8af.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-08e0dc436ad0ad61ba5558db0674d762.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-093b9ef39a46ceae95a1df18a0a3a326.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-095c169f3314805276f603a362766abd.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-09850c4077f3fffe707905872e0e2460.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-0986d134c05864f5025962eef9f994a0.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-0b5d723fdc4e249c140f0909e87d03b4.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-0f626226ba1272e832aea87bafd9720e.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-0f7fb1e0d5015bb1371343153ecf7ce3.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-0fa55a080fcd0f9dc2e0b0058b793df8.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-0facdf1ea213ba40261022f5d5ed4493.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-10a7ae9a371830a80c3d844acf1c02d7.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-112c051027b2d766c19a519f6ee1f4f7.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-11c345711937f0ba4b8f7b6b919c8440.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-12b52b58eb3df36804b9a654ec9ee194.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-13ae07ed2e272d26d59bc0691cd7117a.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-13d2887ec8ee73c43acdabc52a05af7b.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-145aa02cdd91946e67dc934e1acffe75.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-15dc6d811c9cd078f9086a740d5a1038.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-173945821411c09f70c95f98d590e697.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-1b611157cd46bb184d4fa4dae2d6a2b8.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-1e6fd68f1f3902ce48ce8c69df385622.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-1ee544f0f1dac422545c505baa788992.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-1fdc0c67ed57263a80fd108c1f6ccf24.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-203b0e569e3b14aac86a003dc3fa523e.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-20cc1bbf50e7efb442756cb605672c1f.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-20e7bf72fa05de9adf7dbcc7bf51dde6.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-21430ee05a1248901da8d0de08744d47.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-23686f7f29da6e8008c36dd3a80c83d6.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-23ad2d71b280f00b1363b95b7bea94eb.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-23f228f3999c01983860012330e4be08.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-24476a126f129212beb33f66853ea151.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-24a21c1e4449222e8d1898d69ff3a404.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-25b7f38e18f035f96cb5e547bd2bd08c.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-29cec36cd205b211da97acabaa62f055.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-2a26d20a23b00898ce82f09d2ee47c3f.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-2adbc89c11e65905393d3dfc468b9d5b.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-2b7441d46298788ac94e610ffcc709b6.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-2b77e8ebfb2367ab2662396a60e7d320.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-2cf96d082d35ea3d8106851223ad0d16.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-2d43040e86ff03ba677f6f9c04cd0805.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-2e33e8dc771ef5e1d9127d60a6b73679.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-33432927cd87d40cfe393c7482bf221f.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-353f33792a8f60dc69323ddf635a269e.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-36925dfe329a45086cbb7fc5c20d45ac.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-3717077e38f98d89eae729b6c14e56dc.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-3756e81d3e149cf6099163ee79944fec.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-395c35dd584b56b0789f58a0559beaf1.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-3c9de2ae0ea4bc91a510942dfa4be8d2.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-3cc70dbb64df5b21f1326cc24dee2195.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-3e1f8f654357353bf0e04ba5c34b5f7f.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-3e63ed8162808a9e425ed80a8bc79114.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-3eaa538115d76932653c21d8dc28f207.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-4095eb84ef3874e2600247bee0b04026.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-41521fade99856108931b4768b1b2648.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-450da755d5bcb70906e1295e559b9602.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-452225341522a7942f0f6aab1a5c91a3.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-4535823663ad81405188a528d8f2b1a2.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-4806e761d750087c2d734fc64596eaff.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-4a0fdb40036e87b40aa08dd30584cb85.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-4a38cc3e9cf104e69ba246d37f8cf135.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-4aca6a43e59aceee2166b0c7e4e85ef1.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-4bfaa8ffa64c5ee560aa2daba7c9cbd3.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-4dc6d5f188d5c96d44815cd1e81aa885.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-4ddc14ed3eb0c3e46364317dfc0144a3.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-4ee10ae43505e2e0bc62656ced49c0fa.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-4f50e5136e136527280bc902c5817561.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-51502f1206be09c565f1547c406e9558.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-51a0e808bbc8361236ac521a119758a3.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-52a84a22fd1369bffeaf21da2d6158dc.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-5330a2119a716e4e7224ed108b085dac.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-543fa46ace099a7099dad69123399400.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-544fc28abe2c5c30e62383fd4dac255f.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-54acdfc2166ad7fcbd074f75fd4a56ba.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-5572b3513ba8df57a3d5d7303ee6b11b.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-56467a5c8840c4d23a60b2f935114848.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-56a32a7689abd0326e57c10c6c069bb4.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-57862b464a55b18c7bf234ce22907d73.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-583d166e56ba0de4b77eabb47ef67839.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-5882ffa04f32584d26109137e2da4352.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-58fd02350d0bc52cf1ca3bb32ce9766e.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-5935a5775af3d5c6307ac667bd9ae74e.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-59e9ff77b0efaf684bc09274fb6908c9.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-5a1ce3117cfe90c48e8fb4a9a00f694d.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-5a45d991244d4c7140217e1e5f5ca4f4.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-5a7fac4b8b23a6e4e5ba0c9bf1756c91.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-5b0ed6971aaab9c8ad563230bd5471a7.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-5d2898fbc097a7e24c6f38d80587621e.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-603aefd23e350ba7eb124273e3c9bcf1.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-60a3089806700d379f11827ee9843b6b.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-60a41c7e1c68f22424e6d22df544bc82.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-642b26e2e5f5fb780b51b593dbc8c851.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-66493ba5a8367f2928812f446f47b56a.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-670ba603758d94268e8606f240a42e12.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-671a2c20b1eb9e4ef8a192833940e319.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-684d65f1793cac449dde5d59cb3c47fb.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-69c09cc5fa3e55c74fc4821f76909cc3.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-6ae5b42180ad70b971c91e7eefb8eba2.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-6e092f71c1e634059ada0e52abadce67.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-6f3256af8454371776bc46670d33cc65.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-6fe5c5973cc06f74b2387a631ea36b88.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-70c2eb8d64e71a42a834eb857ea9df51.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-70e811fd7994e61f408c923de6ddd078.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-7197d6fda6cba7c3874c53d6381ca239.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-72252d73220fa3cd856677888cee1635.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-72536a3d71b694a0d53dd90ddceae41e.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-726303e0774b4e678bff8c2deb6ca603.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-733171b4ffcd17ea1fe1c0ba627173bf.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-739bc1a567439c7cffcd1614644593d2.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-73e309718fd16cea44b4d54a33581811.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-7494dc504ae00ee9cd0505f990f88c5d.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-74e2263a91439c25b91d5132ce9f4d62.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-761d05e3cd968cf574166867998ef06a.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-7718fe60986d8b42b1be9c5ace5ccf25.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-774d4f764a1299da5d28ec2f2ffe0d69.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-79d494361ae093b69e74ee9dbe65bfd4.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-79f007c1c6d07557120982951ea67998.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-7a07ddc0f0c0f5f4a9bad6ee3dda66b5.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-7ab2bed91166a9dca83a5ebfbe2a7f38.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-7ccce86603f80a099ddb0cb21d4ae3e3.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-7e4bde7e9c7f84cd34d8a845e384c746.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-7e929f262f30c8ee78bf398150b1a7cd.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-7eb9fffd1aa890d07d0f88cc82e6cfe4.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-7f855356ab893b0d2b9c1c83b8116f0e.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-829615148e6357d826b9242eb7fbbd1e.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-866fa7613df6b3fd272bcfd4530c0bb9.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-87599f94b6cc129d505b375798d0d751.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-8c2f33cee3993174f7e87c28e4bf42ee.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-8d3bcabb847b56243b16afe62adaaf21.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-8de5b863cb50dfefdd07cb11c774d579.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-8e9f97f01034820170065b2921b4fb5e.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-8f476c4c99813d57cbe6eca4727388ad.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-903bb6865f3452e2fda42e3a25547bc5.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-91ddb2969bf2d31ba02ad82998d1314c.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-938d90c10ff8c20386af7f242c05d6b0.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-93fc8f28a33234bcadf1527cafabd502.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-95429962233afd82db1c27df1500a28c.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-9544732d2e62d1a429674f8ee41b5d3a.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-9592bfc861f07bcb8d75c196b370e548.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-95bfd249da4902577b4b7d76ebdd0b44.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-968cffdc8ee679da094e77ebf50f58ef.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-97f7f48ce90c9429bf32ae51469db74d.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-982b630266d87db93d2539affb1275c6.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-98f2ad84457de7f3740d9920b8fa8667.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-99a16ef6a64934d5781933dbd9c46b2e.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-9cfb2a77a4e45025105ad29a1748b90d.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-9d81066dd2b337c938df6e90380a00dc.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-9eb5a99df4e76ac3363453ac9ca288b1.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-a004ddfcb26e67bd6e678c8ed19e25ce.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-a0ca5df4258213d7fc9fce80f65ce760.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-a1f916d6039285c4ffb900cd654e418f.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-a203b91dad570bf05a58c3c3ddb529bf.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-a365e82ed54697a52f27adcea1315fe8.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-a4c34be6d42152e64b0df90bc4607f64.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-a7accba310e821da5505f71c03b76bdb.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-aa0d470430e6391eca720c7cfa44446f.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-aa5c9ca6cf4fba00433b7aa3fa10671a.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-ac9ceb44437becc3e9c4dbfebab7fc2d.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-b1220a3c61f85cc0408deedb4c5f57a2.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-b358f7a51ece39a3247942b1feabdb29.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-b57aaedfd8ebdf3931f25119dc6a5eb2.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-b5c1596551c256e0e9cf02028595b092.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-b6d128682ee29e471486354d486a1b90.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-b6fd38ca30869792244804b04bc058da.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-b7d203b051eff504ff59ddca7576b6a9.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-b96d9226ce77ec94ceca043d712182e6.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-ba3de316d63c7e339987b16f41a0b879.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-bafff7a14c27403dcc6cf1432e8ea836.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-bd77e3c7f9e0b072d96af37f73d1aa32.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-be549ab72f0719d606a5c01e2c0219b6.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-c16ed9740b85badf16e86ea782a3062f.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-c1f94158256bb1f3bf665b053d895af9.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-c40533fdf4cc57177b12803598af7e59.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-c4a687ac4f0c2766eefc9f77ed99cddf.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-c69f61a4ab18d0488c8d1fc12e7028e8.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-c8b71798409ccc126ee264a00aadcf21.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-c99eda15fc26a2941579560f76c3a5cf.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-cb17fc3db95f6d139afc9d31a8e93293.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-cbaaefaaf326668277aa24dfa93c4d28.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-cd145ce4a0ea18469358df53c207bc1b.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-cdbce89e82cc1ab53a2decbf5819278f.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-ce4884f96f11589608b76b726a755803.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-cf2cc71752631e579e35b0e423bf2638.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-cf6ff4e0f491ca0cf3038187a997b9b4.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-cfb211578629b7e8153b37240de6a9d5.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-d0cf73942fea1c74edbdf0b3011f4656.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-d2666cbed13462c5dc36fa2f15c202ca.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-d3716376641d615e2995605b29bca7b6.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-dac48066b5883d8b4551fc584f0c2a3e.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-dbea1af6dcd9860be40c3d18254338f5.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-e11567fd2accf9957cd0d3c2be937d87.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-e2ead7ea7da0437f085f42ffc05f8d13.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-e3fcf5180fd466c8915c4e8069491054.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-e480d9c614742d05f0e78f274f1e69e6.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-e4bca6cfa53e499cae0a6be4894a90e9.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-e51ef413167c6e14e0c0fdcc585f2fc9.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-e5f453bb04da18eed01675eeebd88bf8.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-e656f091b9dc4709722c9f4b84d3c797.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-ec181b795ac1fb5a50d700b6e996d745.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-ee8bae97908d5147b423f77ad0d3c1bb.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-f0f13b5c60e0af5553bd359f5513be1b.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-f2b54d4e7be0eaefe1c2c56836fa5368.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-f56414bf9bced67990def8660e306759.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-f5d079153c99a25b9be5b8583c4cc8a7.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-f6032fc06eb20480f096199713f70885.woff2 +0 -0
- package/web/excalidraw/fonts/Xiaolai/Xiaolai-Regular-f8ee5d36068a42b51d0e4a1116cfcec1.woff2 +0 -0
- package/web/fonts/LICENSE-_fontsource-variable_inter.txt +93 -0
- package/web/fonts/LICENSE-_fontsource-variable_jetbrains-mono.txt +93 -0
- package/web/fonts/LICENSE-_fontsource_lxgw-wenkai.txt +94 -0
- package/web/fonts/LICENSE-_fontsource_noto-sans-sc.txt +93 -0
- package/web/fonts/files/inter-cyrillic-ext-wght-normal.woff2 +0 -0
- package/web/fonts/files/inter-cyrillic-wght-normal.woff2 +0 -0
- package/web/fonts/files/inter-greek-ext-wght-normal.woff2 +0 -0
- package/web/fonts/files/inter-greek-wght-normal.woff2 +0 -0
- package/web/fonts/files/inter-latin-ext-wght-normal.woff2 +0 -0
- package/web/fonts/files/inter-latin-wght-normal.woff2 +0 -0
- package/web/fonts/files/inter-vietnamese-wght-normal.woff2 +0 -0
- package/web/fonts/files/jetbrains-mono-cyrillic-ext-wght-normal.woff2 +0 -0
- package/web/fonts/files/jetbrains-mono-cyrillic-wght-normal.woff2 +0 -0
- package/web/fonts/files/jetbrains-mono-greek-wght-normal.woff2 +0 -0
- package/web/fonts/files/jetbrains-mono-latin-ext-wght-normal.woff2 +0 -0
- package/web/fonts/files/jetbrains-mono-latin-wght-normal.woff2 +0 -0
- package/web/fonts/files/jetbrains-mono-vietnamese-wght-normal.woff2 +0 -0
- package/web/fonts/files/lxgw-wenkai-latin-500-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-100-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-100-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-101-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-101-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-102-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-102-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-103-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-103-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-104-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-104-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-105-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-105-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-106-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-106-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-107-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-107-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-108-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-108-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-109-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-109-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-110-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-110-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-111-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-111-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-112-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-112-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-113-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-113-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-114-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-114-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-115-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-115-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-116-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-116-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-117-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-117-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-118-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-118-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-119-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-119-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-21-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-21-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-22-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-22-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-23-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-23-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-24-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-24-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-25-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-25-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-26-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-26-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-27-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-27-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-28-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-28-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-29-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-29-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-30-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-30-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-31-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-31-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-32-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-32-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-33-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-33-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-34-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-34-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-35-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-35-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-36-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-36-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-37-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-37-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-38-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-38-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-39-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-39-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-4-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-4-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-40-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-40-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-41-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-41-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-42-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-42-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-43-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-43-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-44-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-44-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-45-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-45-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-46-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-46-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-47-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-47-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-48-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-48-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-49-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-49-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-5-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-5-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-50-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-50-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-51-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-51-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-52-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-52-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-53-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-53-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-54-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-54-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-55-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-55-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-56-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-56-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-57-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-57-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-58-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-58-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-59-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-59-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-6-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-6-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-60-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-60-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-61-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-61-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-62-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-62-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-63-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-63-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-64-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-64-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-65-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-65-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-66-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-66-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-67-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-67-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-68-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-68-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-69-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-69-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-70-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-70-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-71-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-71-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-72-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-72-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-73-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-73-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-74-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-74-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-75-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-75-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-76-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-76-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-77-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-77-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-78-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-78-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-79-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-79-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-80-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-80-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-81-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-81-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-82-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-82-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-83-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-83-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-84-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-84-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-85-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-85-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-86-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-86-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-87-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-87-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-88-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-88-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-89-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-89-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-90-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-90-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-91-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-91-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-97-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-97-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-98-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-98-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-99-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-99-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-cyrillic-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-cyrillic-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-latin-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-latin-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-latin-ext-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-latin-ext-700-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-vietnamese-400-normal.woff2 +0 -0
- package/web/fonts/files/noto-sans-sc-vietnamese-700-normal.woff2 +0 -0
- package/web/fonts/fonts.css +2163 -0
- package/web/index.html +266 -0
package/main.mjs
ADDED
|
@@ -0,0 +1,3434 @@
|
|
|
1
|
+
// unit/main.ts
|
|
2
|
+
import { mkdir as mkdir7, readFile as readFile7, open as open2, rm as rm4 } from "node:fs/promises";
|
|
3
|
+
import { dirname as dirname4, resolve as resolve7 } from "node:path";
|
|
4
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
5
|
+
import { randomUUID as randomUUID8 } from "node:crypto";
|
|
6
|
+
|
|
7
|
+
// unit/host.ts
|
|
8
|
+
import { readFile as readFile5, mkdir as mkdir3 } from "node:fs/promises";
|
|
9
|
+
import { resolve as resolve3 } from "node:path";
|
|
10
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
13
|
+
|
|
14
|
+
// desktop/electron/unitWeb.ts
|
|
15
|
+
import http from "node:http";
|
|
16
|
+
import { createHash, randomBytes, randomUUID, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
17
|
+
import { extname, normalize, sep } from "node:path";
|
|
18
|
+
import { readFile, realpath } from "node:fs/promises";
|
|
19
|
+
import { createReadStream } from "node:fs";
|
|
20
|
+
|
|
21
|
+
// desktop/shared/amadeus/ipc.ts
|
|
22
|
+
var IPC = {
|
|
23
|
+
openVault: "vault:open",
|
|
24
|
+
restoreVault: "vault:restore",
|
|
25
|
+
listPages: "vault:list",
|
|
26
|
+
listFiles: "vault:files",
|
|
27
|
+
loadPage: "page:load",
|
|
28
|
+
readPage: "page:read",
|
|
29
|
+
newPage: "page:new",
|
|
30
|
+
savePage: "page:save",
|
|
31
|
+
renamePage: "page:rename",
|
|
32
|
+
reconcilePage: "page:reconcile",
|
|
33
|
+
saveAsset: "asset:save",
|
|
34
|
+
saveVaultBytes: "vault:save-bytes",
|
|
35
|
+
readVaultBytes: "vault:read-bytes",
|
|
36
|
+
saveAttachment: "attachment:save",
|
|
37
|
+
openAttachment: "attachment:open",
|
|
38
|
+
copyAttachment: "attachment:copy",
|
|
39
|
+
openVaultFile: "vault:open-file",
|
|
40
|
+
exportPdf: "page:export-pdf",
|
|
41
|
+
exportCsv: "db:export-csv",
|
|
42
|
+
externalChange: "page:external-change",
|
|
43
|
+
search: "vault:search",
|
|
44
|
+
backlinks: "vault:backlinks",
|
|
45
|
+
exclusiveAssets: "vault:exclusive-assets",
|
|
46
|
+
reindex: "vault:reindex",
|
|
47
|
+
listTags: "vault:tags",
|
|
48
|
+
listMarks: "vault:marks",
|
|
49
|
+
patchMark: "vault:patch-mark",
|
|
50
|
+
pagesByTag: "vault:tag-pages",
|
|
51
|
+
deletePage: "page:delete",
|
|
52
|
+
movePage: "page:move",
|
|
53
|
+
resolveEmbed: "embed:resolve",
|
|
54
|
+
blockBacklinks: "embed:backlinks",
|
|
55
|
+
listFolders: "vault:folders",
|
|
56
|
+
createFolder: "folder:create",
|
|
57
|
+
renameFolder: "folder:rename",
|
|
58
|
+
deleteFolder: "folder:delete",
|
|
59
|
+
moveFolder: "folder:move",
|
|
60
|
+
trashEntry: "trash:put",
|
|
61
|
+
listTrash: "trash:list",
|
|
62
|
+
restoreTrash: "trash:restore",
|
|
63
|
+
deleteTrashEntry: "trash:delete",
|
|
64
|
+
emptyTrash: "trash:empty",
|
|
65
|
+
pageIcons: "vault:page-icons",
|
|
66
|
+
pageAliases: "vault:page-aliases",
|
|
67
|
+
unlinkedMentions: "vault:unlinked-mentions",
|
|
68
|
+
linkMention: "vault:link-mention",
|
|
69
|
+
fetchLinkMeta: "web:link-meta",
|
|
70
|
+
searchImages: "web:search-images",
|
|
71
|
+
structureChange: "vault:structure-change",
|
|
72
|
+
/** 渲染层的路径广播(改名 / 挪走 / 删除)跨窗口转发:渲染层 invoke 进主进程,主进程转给**其它**窗口。 */
|
|
73
|
+
pathGone: "vault:path-gone",
|
|
74
|
+
listPlugins: "plugins:list",
|
|
75
|
+
openPluginsFolder: "plugins:open-folder",
|
|
76
|
+
scaffoldPlugin: "plugins:scaffold",
|
|
77
|
+
uninstallPlugin: "plugins:uninstall-forsion",
|
|
78
|
+
bundleAgentOwned: "plugins:bundle-agent-owned",
|
|
79
|
+
revealInFileManager: "shell:reveal",
|
|
80
|
+
dbRead: "db:read",
|
|
81
|
+
dbWrite: "db:write",
|
|
82
|
+
dbWriteCas: "db:write-cas",
|
|
83
|
+
dbChange: "db:external-change",
|
|
84
|
+
drawingRead: "drawing:read",
|
|
85
|
+
drawingWrite: "drawing:write",
|
|
86
|
+
// 通用 vault 文本文件读写(供插件文件类型 ctx.app.readFile/writeFile;写走自写账本防 watcher 回弹)。
|
|
87
|
+
readTextFile: "file:read-text",
|
|
88
|
+
writeTextFile: "file:write-text",
|
|
89
|
+
/** 外部改动了 vault 里的**非 .md / 非 .db** 文件(附件、片段 .js…)→ ctx.app.watchFile 的数据源。 */
|
|
90
|
+
fileChange: "file:external-change",
|
|
91
|
+
/** 每插件一份 JSON blob(~/.forsion/plugins-data/<id>.json);ctx.loadData/saveData 的落点。 */
|
|
92
|
+
pluginDataRead: "plugins:data-read",
|
|
93
|
+
pluginDataWrite: "plugins:data-write",
|
|
94
|
+
setPageFrontmatter: "page:set-frontmatter",
|
|
95
|
+
listPageProps: "vault:page-props",
|
|
96
|
+
renamePageFile: "page:rename-file",
|
|
97
|
+
renameDbFile: "db:rename-file",
|
|
98
|
+
/** 页面版本历史(评审 C-20):快照存在库外(主进程 tanguDataDir()/amadeus-history),只有桌面主进程实现。 */
|
|
99
|
+
listPageHistory: "page:history-list",
|
|
100
|
+
readPageHistory: "page:history-read",
|
|
101
|
+
restorePageHistory: "page:history-restore"
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// desktop/products/forsion.json
|
|
105
|
+
var forsion_default = {
|
|
106
|
+
id: "forsion",
|
|
107
|
+
displayName: "Forsion",
|
|
108
|
+
appId: "com.forsion.tangu-desktop2",
|
|
109
|
+
productName: "Forsion",
|
|
110
|
+
artifactPrefix: "Forsion",
|
|
111
|
+
defaultSpace: "tangu",
|
|
112
|
+
spaces: [
|
|
113
|
+
"home",
|
|
114
|
+
"tangu",
|
|
115
|
+
"inbox",
|
|
116
|
+
"amadeus",
|
|
117
|
+
"calendar",
|
|
118
|
+
"coding",
|
|
119
|
+
"artificial",
|
|
120
|
+
"automation",
|
|
121
|
+
"muse",
|
|
122
|
+
"public"
|
|
123
|
+
],
|
|
124
|
+
agentBackend: true,
|
|
125
|
+
market: true
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// desktop/shared/product.ts
|
|
129
|
+
function resolveProduct(build, runtime) {
|
|
130
|
+
const profile = build ?? runtime ?? forsion_default;
|
|
131
|
+
return { ...profile, unit: profile.unit ?? profile.agentBackend };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// desktop/electron/product.ts
|
|
135
|
+
var PRODUCT = resolveProduct(typeof __FORSION_PRODUCT__ === "undefined" ? void 0 : __FORSION_PRODUCT__);
|
|
136
|
+
|
|
137
|
+
// desktop/electron/engineRoutes.generated.ts
|
|
138
|
+
var ENGINE_ROUTES = [
|
|
139
|
+
{ method: "GET", path: "/agent/agents", access: "allow", src: "routes/agents.ts", why: "Agent roster; remote callers get no persona / developer instructions and the approval mode clamped to the remote cap (P1-M1A projection)" },
|
|
140
|
+
{ method: "POST", path: "/agent/agents", access: "deny-remote", src: "routes/agents.ts", why: "create Agent (design appendix C)" },
|
|
141
|
+
{ method: "GET", path: "/agent/agents-meta", access: "allow", src: "routes/agents.ts", why: "Agent roster meta" },
|
|
142
|
+
{ method: "PUT", path: "/agent/agents-meta", access: "deny-remote", src: "routes/agents.ts", why: "Agent roster meta write" },
|
|
143
|
+
{ method: "DELETE", path: "/agent/agents/:slug", access: "deny-remote", src: "routes/agents.ts", why: "delete Agent" },
|
|
144
|
+
{ method: "PATCH", path: "/agent/agents/:slug", access: "deny-remote", src: "routes/agents.ts", why: "update Agent (design appendix C)" },
|
|
145
|
+
{ method: "DELETE", path: "/agent/agents/:slug/avatar", access: "deny-remote", src: "routes/agents.ts", why: "update Agent avatar" },
|
|
146
|
+
{ method: "GET", path: "/agent/agents/:slug/avatar", access: "allow", src: "routes/agents.ts", why: "Agent avatar" },
|
|
147
|
+
{ method: "POST", path: "/agent/agents/:slug/avatar", access: "deny-remote", src: "routes/agents.ts", why: "update Agent avatar" },
|
|
148
|
+
{ method: "GET", path: "/agent/agents/:slug/harness", access: "allow", src: "routes/agents.ts", why: "read HARNESS" },
|
|
149
|
+
{ method: "POST", path: "/agent/agents/:slug/harness/rollback", access: "deny-remote", src: "routes/agents.ts", why: "harness rollback (design appendix C)" },
|
|
150
|
+
{ method: "GET", path: "/agent/agents/:slug/library", access: "allow", src: "routes/agents.ts", why: "Agent library listing" },
|
|
151
|
+
{ method: "DELETE", path: "/agent/agents/:slug/library/file", access: "deny-remote", src: "routes/agents.ts", why: "deletes from the Agent library" },
|
|
152
|
+
{ method: "GET", path: "/agent/agents/:slug/library/file", access: "allow", src: "routes/agents.ts", why: "Agent library file" },
|
|
153
|
+
{ method: "POST", path: "/agent/agents/:slug/library/file", access: "deny-remote", src: "routes/agents.ts", why: "writes into the Agent library" },
|
|
154
|
+
{ method: "GET", path: "/agent/agents/:slug/log", access: "allow", src: "routes/agents.ts", why: "Agent log" },
|
|
155
|
+
{ method: "PUT", path: "/agent/agents/:slug/log", access: "deny-remote", src: "routes/agents.ts", why: "memory log write" },
|
|
156
|
+
{ method: "GET", path: "/agent/agents/:slug/logs", access: "allow", src: "routes/agents.ts", why: "Agent log list" },
|
|
157
|
+
{ method: "GET", path: "/agent/agents/:slug/memory", access: "allow", src: "routes/agents.ts", why: "read Agent memory" },
|
|
158
|
+
{ method: "PUT", path: "/agent/agents/:slug/memory", access: "deny-remote", src: "routes/agents.ts", why: "memory write (design appendix C)" },
|
|
159
|
+
{ method: "POST", path: "/agent/agents/:slug/memory/entries", access: "deny-remote", src: "routes/agents.ts", why: "memory write (design appendix C)" },
|
|
160
|
+
{ method: "POST", path: "/agent/agents/:slug/memory/restore", access: "deny-remote", src: "routes/agents.ts", why: "memory write" },
|
|
161
|
+
{ method: "GET", path: "/agent/agents/:slug/memory/revisions", access: "allow", src: "routes/agents.ts", why: "memory revisions" },
|
|
162
|
+
{ method: "POST", path: "/agent/agents/:slug/rename", access: "deny-remote", src: "routes/agents.ts", why: "rename Agent" },
|
|
163
|
+
{ method: "GET", path: "/agent/tool-catalog", access: "allow", src: "routes/agents.ts", why: "tool catalog" },
|
|
164
|
+
{ method: "GET", path: "/agent/user-profile", access: "allow", src: "routes/agents.ts", why: "read user profile" },
|
|
165
|
+
{ method: "PUT", path: "/agent/user-profile", access: "deny-remote", src: "routes/agents.ts", why: "user profile write (design appendix C)" },
|
|
166
|
+
{ method: "GET", path: "/agent/approval-rules", access: "deny-remote", src: "routes/approvals.ts", why: "approval rules are local policy" },
|
|
167
|
+
{ method: "PUT", path: "/agent/approval-rules", access: "deny-remote", src: "routes/approvals.ts", why: "approval rules are local policy" },
|
|
168
|
+
{ method: "GET", path: "/agent/approvals/pending", access: "allow", src: "routes/approvals.ts", why: "per-session pending counts for the session-list badge (no ids / previews)" },
|
|
169
|
+
{ method: "GET", path: "/agent/approvals/stream", access: "deny-remote", src: "routes/approvals.ts", why: "engine-wide pending approval feed for the host main process only" },
|
|
170
|
+
{ method: "POST", path: "/agent/runs/:runId/approvals/:approvalId", access: "allow", src: "routes/approvals.ts", why: "answer an approval (D1: remote approval allowed; bound to its run by P0 \u2461)" },
|
|
171
|
+
{ method: "POST", path: "/agent/runs/:runId/captures/:shotId", access: "allow", src: "routes/approvals.ts", why: "Agent Desk capture upload for a run (shotId bound to run, P0 \u2461)" },
|
|
172
|
+
{ method: "POST", path: "/agent/runs/:runId/inquiries/:inquiryId", access: "allow", src: "routes/approvals.ts", why: "answer an inquiry (D1)" },
|
|
173
|
+
{ method: "GET", path: "/agent/skills", access: "allow", src: "routes/assets.ts", why: "skills list" },
|
|
174
|
+
{ method: "GET", path: "/agent/skills/catalog", access: "allow", src: "routes/assets.ts", why: "skill catalog" },
|
|
175
|
+
{ method: "POST", path: "/agent/skills/catalog", access: "deny-remote", src: "routes/assets.ts", why: "create skill" },
|
|
176
|
+
{ method: "DELETE", path: "/agent/skills/catalog/:key", access: "deny-remote", src: "routes/assets.ts", why: "delete skill" },
|
|
177
|
+
{ method: "GET", path: "/agent/skills/catalog/:key", access: "allow", src: "routes/assets.ts", why: "skill detail" },
|
|
178
|
+
{ method: "PATCH", path: "/agent/skills/catalog/:key", access: "deny-remote", src: "routes/assets.ts", why: "edit skill" },
|
|
179
|
+
{ method: "POST", path: "/agent/skills/catalog/:key/copy", access: "deny-remote", src: "routes/assets.ts", why: "copy skill between Agents" },
|
|
180
|
+
{ method: "PUT", path: "/agent/skills/catalog/:key/disabled", access: "deny-remote", src: "routes/assets.ts", why: "skill enablement config" },
|
|
181
|
+
{ method: "POST", path: "/agent/skills/catalog/import", access: "deny-remote", src: "routes/assets.ts", why: "skill import (design appendix C)" },
|
|
182
|
+
{ method: "POST", path: "/agent/skills/upload", access: "deny-remote", src: "routes/assets.ts", why: "skill upload (design appendix C)" },
|
|
183
|
+
{ method: "DELETE", path: "/agent/skills/user/:id", access: "deny-remote", src: "routes/assets.ts", why: "delete user skill" },
|
|
184
|
+
{ method: "GET", path: "/agent/tools", access: "allow", src: "routes/assets.ts", why: "tool list" },
|
|
185
|
+
{ method: "GET", path: "/agent/browser-extension", access: "deny-remote", src: "routes/browserExtension.ts", why: "browser extension pairing credentials" },
|
|
186
|
+
{ method: "POST", path: "/agent/browser-extension/reset-code", access: "deny-remote", src: "routes/browserExtension.ts", why: "rotates browser extension pairing credentials" },
|
|
187
|
+
{ method: "GET", path: "/agent/channels", access: "deny-remote", src: "routes/channels.ts", why: "channel config is local-only" },
|
|
188
|
+
{ method: "PUT", path: "/agent/channels/:kind/config", access: "deny-remote", src: "routes/channels.ts", why: "channel config is local-only" },
|
|
189
|
+
{ method: "POST", path: "/agent/channels/:kind/connect", access: "deny-remote", src: "routes/channels.ts", why: "channel binding is local-only (design appendix C)" },
|
|
190
|
+
{ method: "POST", path: "/agent/channels/:kind/connect-session", access: "deny-remote", src: "routes/channels.ts", why: "channel binding is local-only (design appendix C)" },
|
|
191
|
+
{ method: "POST", path: "/agent/channels/:kind/disconnect", access: "deny-remote", src: "routes/channels.ts", why: "channel binding is local-only" },
|
|
192
|
+
{ method: "POST", path: "/agent/channels/:kind/sessions/new", access: "deny-remote", src: "routes/channels.ts", why: "channel binding is local-only (design appendix C)" },
|
|
193
|
+
{ method: "GET", path: "/agent/commands", access: "allow", src: "routes/commands.ts", why: "slash command catalog" },
|
|
194
|
+
{ method: "POST", path: "/agent/commands/:name/expand", access: "allow", src: "routes/commands.ts", why: "expand a custom slash command template (read-only)" },
|
|
195
|
+
{ method: "GET", path: "/agent/engines", access: "deny-remote", src: "routes/engines.ts", why: "external engines are local-only (engineId is stripped from remote runs)" },
|
|
196
|
+
{ method: "PUT", path: "/agent/engines/:id", access: "deny-remote", src: "routes/engines.ts", why: "external engine config" },
|
|
197
|
+
{ method: "GET", path: "/agent/engines/:id/assets", access: "deny-remote", src: "routes/engines.ts", why: "reads host assets of an external engine" },
|
|
198
|
+
{ method: "GET", path: "/agent/engines/:id/capabilities", access: "deny-remote", src: "routes/engines.ts", why: "external engines are local-only" },
|
|
199
|
+
{ method: "POST", path: "/agent/engines/:id/import", access: "deny-remote", src: "routes/engines.ts", why: "imports external engine assets" },
|
|
200
|
+
{ method: "GET", path: "/agent/hooks", access: "deny-remote", src: "routes/hooks.ts", why: "hooks run host commands" },
|
|
201
|
+
{ method: "PUT", path: "/agent/hooks", access: "deny-remote", src: "routes/hooks.ts", why: "hooks run host commands" },
|
|
202
|
+
{ method: "POST", path: "/agent/hooks/enable", access: "deny-remote", src: "routes/hooks.ts", why: "hooks run host commands" },
|
|
203
|
+
{ method: "POST", path: "/agent/hooks/trust", access: "deny-remote", src: "routes/hooks.ts", why: "hooks run host commands" },
|
|
204
|
+
{ method: "GET", path: "/agent/agents/:slug/human", access: "deny-remote", src: "routes/human.ts", why: "host collaboration configuration" },
|
|
205
|
+
{ method: "PUT", path: "/agent/agents/:slug/human", access: "deny-remote", src: "routes/human.ts", why: "host collaboration configuration write" },
|
|
206
|
+
{ method: "POST", path: "/agent/agents/:slug/human/undo", access: "deny-remote", src: "routes/human.ts", why: "host collaboration configuration undo" },
|
|
207
|
+
{ method: "GET", path: "/agent/project-context/human", access: "deny-remote", src: "routes/human.ts", why: "host collaboration configuration" },
|
|
208
|
+
{ method: "PUT", path: "/agent/project-context/human", access: "deny-remote", src: "routes/human.ts", why: "host collaboration configuration write" },
|
|
209
|
+
{ method: "POST", path: "/agent/project-context/human/undo", access: "deny-remote", src: "routes/human.ts", why: "host collaboration configuration undo" },
|
|
210
|
+
{ method: "GET", path: "/agent/inbox", access: "allow", src: "routes/inbox.ts", why: "inbox list" },
|
|
211
|
+
{ method: "POST", path: "/agent/inbox", access: "deny-remote", src: "routes/inbox.ts", why: "posts a system-sender message and forwards it to channels" },
|
|
212
|
+
{ method: "DELETE", path: "/agent/inbox/:id", access: "allow", src: "routes/inbox.ts", why: "soft-delete an inbox message" },
|
|
213
|
+
{ method: "PATCH", path: "/agent/inbox/:id", access: "allow", src: "routes/inbox.ts", why: "mark read / star" },
|
|
214
|
+
{ method: "POST", path: "/agent/inbox/:id/claim", access: "allow", src: "routes/inbox.ts", why: "claim a reward for the same account" },
|
|
215
|
+
{ method: "POST", path: "/agent/inbox/pull", access: "allow", src: "routes/inbox.ts", why: "pull cloud broadcasts" },
|
|
216
|
+
{ method: "POST", path: "/agent/inbox/read-all", access: "allow", src: "routes/inbox.ts", why: "mark all read" },
|
|
217
|
+
{ method: "GET", path: "/agent/inbox/unread-count", access: "allow", src: "routes/inbox.ts", why: "inbox badge" },
|
|
218
|
+
{ method: "POST", path: "/agent/inline", access: "allow", src: "routes/inline.ts", why: "inline writing completion for the note editor (no tools, nothing persisted; same shape as aside)" },
|
|
219
|
+
{ method: "GET", path: "/agent/log", access: "allow", src: "routes/memory.ts", why: "read daily log" },
|
|
220
|
+
{ method: "POST", path: "/agent/log", access: "deny-remote", src: "routes/memory.ts", why: "memory log write" },
|
|
221
|
+
{ method: "GET", path: "/agent/memory", access: "allow", src: "routes/memory.ts", why: "read memory" },
|
|
222
|
+
{ method: "POST", path: "/agent/memory", access: "deny-remote", src: "routes/memory.ts", why: "memory write" },
|
|
223
|
+
{ method: "POST", path: "/agent/sync", access: "deny-remote", src: "routes/memory.ts", why: "memory sync (design appendix C)" },
|
|
224
|
+
{ method: "GET", path: "/agent/sync/status", access: "allow", src: "routes/memory.ts", why: "memory sync status" },
|
|
225
|
+
{ method: "DELETE", path: "/agent/agents/:slug/memory/dream", access: "deny-remote", src: "routes/memoryMaintenance.ts", why: "memory maintenance control" },
|
|
226
|
+
{ method: "GET", path: "/agent/agents/:slug/memory/dream", access: "allow", src: "routes/memoryMaintenance.ts", why: "read memory maintenance state" },
|
|
227
|
+
{ method: "POST", path: "/agent/agents/:slug/memory/dream", access: "deny-remote", src: "routes/memoryMaintenance.ts", why: "starts a memory rewrite" },
|
|
228
|
+
{ method: "PUT", path: "/agent/agents/:slug/memory/dream", access: "deny-remote", src: "routes/memoryMaintenance.ts", why: "memory maintenance config" },
|
|
229
|
+
{ method: "GET", path: "/agent/models", access: "allow", src: "routes/models.ts", why: "model list" },
|
|
230
|
+
{ method: "PUT", path: "/agent/models/overrides", access: "deny-remote", src: "routes/models.ts", why: "model overrides config" },
|
|
231
|
+
{ method: "GET", path: "/agent/plugins", access: "deny-remote", src: "routes/plugins.ts", why: "engine plugins are local-only" },
|
|
232
|
+
{ method: "DELETE", path: "/agent/plugins/:id", access: "deny-remote", src: "routes/plugins.ts", why: "engine plugins are local-only" },
|
|
233
|
+
{ method: "PUT", path: "/agent/plugins/:id/enabled", access: "deny-remote", src: "routes/plugins.ts", why: "engine plugins are local-only" },
|
|
234
|
+
{ method: "DELETE", path: "/agent/plugins/:id/files", access: "deny-remote", src: "routes/plugins.ts", why: "engine plugins are local-only" },
|
|
235
|
+
{ method: "GET", path: "/agent/plugins/:id/files", access: "deny-remote", src: "routes/plugins.ts", why: "engine plugins are local-only" },
|
|
236
|
+
{ method: "POST", path: "/agent/plugins/:id/files", access: "deny-remote", src: "routes/plugins.ts", why: "engine plugins are local-only" },
|
|
237
|
+
{ method: "GET", path: "/agent/plugins/:id/settings", access: "deny-remote", src: "routes/plugins.ts", why: "plugin settings may hold secrets" },
|
|
238
|
+
{ method: "PUT", path: "/agent/plugins/:id/settings", access: "deny-remote", src: "routes/plugins.ts", why: "engine plugins are local-only" },
|
|
239
|
+
{ method: "GET", path: "/agent/plugins/:id/source", access: "deny-remote", src: "routes/plugins.ts", why: "engine plugins are local-only" },
|
|
240
|
+
{ method: "POST", path: "/agent/plugins/install", access: "deny-remote", src: "routes/plugins.ts", why: "installs code (design appendix C)" },
|
|
241
|
+
{ method: "POST", path: "/agent/plugins/rescan", access: "deny-remote", src: "routes/plugins.ts", why: "engine plugins are local-only" },
|
|
242
|
+
{ method: "POST", path: "/agent/reply-segments", access: "allow", src: "routes/plugins.ts", why: "chat bubble segmentation helper (read-only)" },
|
|
243
|
+
{ method: "GET", path: "/agent/git-settings", access: "allow", src: "routes/projectContext.ts", why: "read Settings \u2192 Git" },
|
|
244
|
+
{ method: "PUT", path: "/agent/git-settings", access: "deny-remote", src: "routes/projectContext.ts", why: "engine config.json write" },
|
|
245
|
+
{ method: "GET", path: "/agent/project-context", access: "allow", src: "routes/projectContext.ts", why: "read project context" },
|
|
246
|
+
{ method: "PUT", path: "/agent/project-context/doc", access: "deny-remote", src: "routes/projectContext.ts", why: "project instruction write (design appendix C)" },
|
|
247
|
+
{ method: "POST", path: "/agent/project-context/git/branch", access: "deny-remote", src: "routes/projectContext.ts", why: "git repository write" },
|
|
248
|
+
{ method: "POST", path: "/agent/project-context/git/commit", access: "deny-remote", src: "routes/projectContext.ts", why: "git commit (runs repository hooks)" },
|
|
249
|
+
{ method: "POST", path: "/agent/project-context/git/init", access: "deny-remote", src: "routes/projectContext.ts", why: "git repository write" },
|
|
250
|
+
{ method: "POST", path: "/agent/project-context/git/message", access: "deny-remote", src: "routes/projectContext.ts", why: "spends model quota; may run trusted repo filters" },
|
|
251
|
+
{ method: "POST", path: "/agent/project-context/git/pending", access: "deny-remote", src: "routes/projectContext.ts", why: "may run trusted repo filters (trust=true)" },
|
|
252
|
+
{ method: "POST", path: "/agent/project-context/git/push", access: "deny-remote", src: "routes/projectContext.ts", why: "git push with local credentials" },
|
|
253
|
+
{ method: "POST", path: "/agent/project-context/git/trust", access: "deny-remote", src: "routes/projectContext.ts", why: "trusts repo-provided executable git config" },
|
|
254
|
+
{ method: "DELETE", path: "/agent/project-context/icon", access: "deny-remote", src: "routes/projectContext.ts", why: "project icon write" },
|
|
255
|
+
{ method: "GET", path: "/agent/project-context/icon", access: "allow", src: "routes/projectContext.ts", why: "project icon" },
|
|
256
|
+
{ method: "POST", path: "/agent/project-context/icon", access: "deny-remote", src: "routes/projectContext.ts", why: "project icon write" },
|
|
257
|
+
{ method: "POST", path: "/agent/project-context/init", access: "deny-remote", src: "routes/projectContext.ts", why: "project instruction write (design appendix C)" },
|
|
258
|
+
{ method: "GET", path: "/agent/project-context/settings", access: "allow", src: "routes/projectContext.ts", why: "read project settings" },
|
|
259
|
+
{ method: "PUT", path: "/agent/project-context/settings", access: "deny-remote", src: "routes/projectContext.ts", why: "project settings write (design appendix C)" },
|
|
260
|
+
{ method: "POST", path: "/agent/project-context/skills", access: "deny-remote", src: "routes/projectContext.ts", why: "project skill write (design appendix C)" },
|
|
261
|
+
{ method: "GET", path: "/agent/providers", access: "deny-remote", src: "routes/providers.ts", why: "provider config (device page uses /unit/providers, keys stripped)" },
|
|
262
|
+
{ method: "POST", path: "/agent/providers/fetch-models", access: "deny-remote", src: "routes/providers.ts", why: "provider config (design appendix C)" },
|
|
263
|
+
{ method: "POST", path: "/agent/providers/test", access: "deny-remote", src: "routes/providers.ts", why: "provider config" },
|
|
264
|
+
{ method: "GET", path: "/agent/websearch", access: "deny-remote", src: "routes/providers.ts", why: "web search config" },
|
|
265
|
+
{ method: "PUT", path: "/agent/websearch", access: "deny-remote", src: "routes/providers.ts", why: "web search config (design appendix C)" },
|
|
266
|
+
{ method: "POST", path: "/agent/websearch/test", access: "deny-remote", src: "routes/providers.ts", why: "web search config" },
|
|
267
|
+
{ method: "GET", path: "/agent/remote/activity", access: "deny-remote", src: "routes/remote.ts", why: "in-flight run registry for the host main process (tray / keep-awake)" },
|
|
268
|
+
{ method: "GET", path: "/agent/remote/activity/events", access: "deny-remote", src: "routes/remote.ts", why: "in-flight run registry feed for the host main process" },
|
|
269
|
+
{ method: "POST", path: "/agent/remote/estop", access: "deny-remote", src: "routes/remote.ts", why: "emergency stop is a host-only action (design \xA76.5)" },
|
|
270
|
+
{ method: "GET", path: "/agent/remote/restart-status", access: "deny-remote", src: "routes/remote.ts", why: "host-only exit checklist (running tasks / background processes) before restarting to update" },
|
|
271
|
+
{ method: "POST", path: "/agent/remote/unlock", access: "deny-remote", src: "routes/remote.ts", why: "unlocking remote access needs local system auth on the host (design \xA76.1, D13)" },
|
|
272
|
+
{ method: "GET", path: "/agent/runs", access: "allow", src: "routes/runs.ts", why: "list runs" },
|
|
273
|
+
{ method: "POST", path: "/agent/runs", access: "allow", src: "routes/runs.ts", why: "start a run (field / approval-mode clamping is engine-side, P0 \u2463)" },
|
|
274
|
+
{ method: "POST", path: "/agent/runs/:id/abort", access: "allow", src: "routes/runs.ts", why: "stop a run" },
|
|
275
|
+
{ method: "GET", path: "/agent/runs/:id/events", access: "allow", src: "routes/runs.ts", why: "run event stream" },
|
|
276
|
+
{ method: "POST", path: "/agent/runs/:id/steer", access: "allow", src: "routes/runs.ts", why: "steer a running run" },
|
|
277
|
+
{ method: "DELETE", path: "/agent/runs/:id/steer/:messageId", access: "allow", src: "routes/runs.ts", why: "withdraw a queued steer message" },
|
|
278
|
+
{ method: "GET", path: "/agent/compaction", access: "allow", src: "routes/sessions.ts", why: "read compaction settings" },
|
|
279
|
+
{ method: "PUT", path: "/agent/compaction", access: "deny-remote", src: "routes/sessions.ts", why: "global compaction config" },
|
|
280
|
+
{ method: "GET", path: "/agent/sessions", access: "allow", src: "routes/sessions.ts", why: "session list" },
|
|
281
|
+
{ method: "POST", path: "/agent/sessions", access: "allow", src: "routes/sessions.ts", why: "create a session" },
|
|
282
|
+
{ method: "DELETE", path: "/agent/sessions/:id", access: "deny-remote", src: "routes/sessions.ts", why: "irreversible hard delete of a local session (runs, messages, code checkpoints); remote uses archive (PATCH archived)" },
|
|
283
|
+
{ method: "PATCH", path: "/agent/sessions/:id", access: "allow", src: "routes/sessions.ts", why: "rename / archive / model / project of a session (a remote project_path never widens /unit/host* reads: only locally confirmed project roots count)" },
|
|
284
|
+
{ method: "POST", path: "/agent/sessions/:id/aside", access: "allow", src: "routes/sessions.ts", why: "btw aside (taint propagates, C5)" },
|
|
285
|
+
{ method: "GET", path: "/agent/sessions/:id/background", access: "allow", src: "routes/sessions.ts", why: "background child sessions" },
|
|
286
|
+
{ method: "POST", path: "/agent/sessions/:id/branch", access: "allow", src: "routes/sessions.ts", why: "branch a session (remote taint propagates, C5)" },
|
|
287
|
+
{ method: "GET", path: "/agent/sessions/:id/checkpoints", access: "allow", src: "routes/sessions.ts", why: "list code checkpoints" },
|
|
288
|
+
{ method: "POST", path: "/agent/sessions/:id/checkpoints/restore", access: "deny-remote", src: "routes/sessions.ts", why: "restores host files from checkpoints (design appendix C)" },
|
|
289
|
+
{ method: "POST", path: "/agent/sessions/:id/compact", access: "allow", src: "routes/sessions.ts", why: "compact a session" },
|
|
290
|
+
{ method: "GET", path: "/agent/sessions/:id/config", access: "allow", src: "routes/sessions.ts", why: "read session config" },
|
|
291
|
+
{ method: "PATCH", path: "/agent/sessions/:id/config", access: "allow", src: "routes/sessions.ts", why: "session config patch (field subset + approval ceiling enforced engine-side, P0 \u2463 / C3)" },
|
|
292
|
+
{ method: "PUT", path: "/agent/sessions/:id/config", access: "deny-remote", src: "routes/sessions.ts", why: "whole-config replace; remote uses PATCH" },
|
|
293
|
+
{ method: "GET", path: "/agent/sessions/:id/detail", access: "allow", src: "routes/sessions.ts", why: "session detail" },
|
|
294
|
+
{ method: "GET", path: "/agent/sessions/:id/messages", access: "allow", src: "routes/sessions.ts", why: "session messages" },
|
|
295
|
+
{ method: "POST", path: "/agent/sessions/:id/messages/delete", access: "deny-remote", src: "routes/sessions.ts", why: "irreversible deletion of local conversation history" },
|
|
296
|
+
{ method: "POST", path: "/agent/sessions/:id/team-members/:slug", access: "allow", src: "routes/sessions.ts", why: "open a team member child session (taint propagates, C5)" },
|
|
297
|
+
{ method: "GET", path: "/agent/sessions/:id/timeline", access: "allow", src: "routes/sessions.ts", why: "session timeline" },
|
|
298
|
+
{ method: "GET", path: "/agent/sessions/:id/usage", access: "allow", src: "routes/sessions.ts", why: "session usage" },
|
|
299
|
+
{ method: "GET", path: "/agent/sessions/search", access: "allow", src: "routes/sessions.ts", why: "session search" },
|
|
300
|
+
{ method: "POST", path: "/agent/solo/:kind/:id/open", access: "allow", src: "routes/solo.ts", why: "open the Agent direct-chat session" },
|
|
301
|
+
{ method: "POST", path: "/agent/solo/:kind/:id/rotate", access: "allow", src: "routes/solo.ts", why: "start a fresh Agent direct-chat session" },
|
|
302
|
+
{ method: "GET", path: "/agent/special/approvals", access: "allow", src: "routes/special.ts", why: "pending async approvals" },
|
|
303
|
+
{ method: "GET", path: "/agent/special/approvals/:id", access: "allow", src: "routes/special.ts", why: "pending async approval" },
|
|
304
|
+
{ method: "POST", path: "/agent/special/approvals/:id/approve", access: "allow", src: "routes/special.ts", why: "answer an async approval (D1/D2)" },
|
|
305
|
+
{ method: "POST", path: "/agent/special/approvals/:id/reject", access: "allow", src: "routes/special.ts", why: "answer an async approval (D1/D2)" },
|
|
306
|
+
{ method: "GET", path: "/agent/special/automation/actions", access: "allow", src: "routes/special.ts", why: "automation action catalog" },
|
|
307
|
+
{ method: "GET", path: "/agent/special/automation/executions", access: "allow", src: "routes/special.ts", why: "automation executions" },
|
|
308
|
+
{ method: "POST", path: "/agent/special/automation/kick", access: "deny-remote", src: "routes/special.ts", why: "kicks automations (design appendix C)" },
|
|
309
|
+
{ method: "GET", path: "/agent/special/automation/runs", access: "allow", src: "routes/special.ts", why: "automation runs" },
|
|
310
|
+
{ method: "GET", path: "/agent/special/automation/sessions", access: "allow", src: "routes/special.ts", why: "automation sessions" },
|
|
311
|
+
{ method: "POST", path: "/agent/special/automation/triggers/:id/fire", access: "deny-remote", src: "routes/special.ts", why: "fires an automation (design appendix C)" },
|
|
312
|
+
{ method: "GET", path: "/agent/special/config", access: "allow", src: "routes/special.ts", why: "renderer auth probe; remote callers get only on/off + two cadence values (P1-K10b projection)" },
|
|
313
|
+
{ method: "POST", path: "/agent/special/config", access: "deny-remote", src: "routes/special.ts", why: "special-agent config write (design appendix C)" },
|
|
314
|
+
{ method: "GET", path: "/agent/special/historian/activity", access: "allow", src: "routes/special.ts", why: "Historian activity" },
|
|
315
|
+
{ method: "POST", path: "/agent/special/muse/feedback", access: "deny-remote", src: "routes/special.ts", why: "appends to Muse LOG (memory write into an autonomous Agent)" },
|
|
316
|
+
{ method: "GET", path: "/agent/special/muse/library", access: "allow", src: "routes/special.ts", why: "Muse library" },
|
|
317
|
+
{ method: "GET", path: "/agent/special/muse/library/file", access: "allow", src: "routes/special.ts", why: "Muse library file (read-only, realpath-bounded to the Library)" },
|
|
318
|
+
{ method: "GET", path: "/agent/special/muse/status", access: "allow", src: "routes/special.ts", why: "Muse status; remote callers get the fields the Muse view / Space read, no absolute paths, budgets or last error (P1-M1A projection)" },
|
|
319
|
+
{ method: "GET", path: "/agent/special/muse/todos", access: "allow", src: "routes/special.ts", why: "Muse todos" },
|
|
320
|
+
{ method: "GET", path: "/agent/special/muse/todos/:id", access: "allow", src: "routes/special.ts", why: "Muse todo" },
|
|
321
|
+
{ method: "PATCH", path: "/agent/special/muse/todos/:id", access: "allow", src: "routes/special.ts", why: "Muse todo status only (card landing / dismiss)" },
|
|
322
|
+
{ method: "POST", path: "/agent/special/muse/todos/:id/approve", access: "allow", src: "routes/special.ts", why: "approve a Muse todo (D1/D2)" },
|
|
323
|
+
{ method: "POST", path: "/agent/special/muse/todos/inject", access: "deny-remote", src: "routes/special.ts", why: "creates persistent follow-up execution (design appendix C)" },
|
|
324
|
+
{ method: "GET", path: "/agent/special/muse/triggers", access: "allow", src: "routes/special.ts", why: "Muse triggers" },
|
|
325
|
+
{ method: "POST", path: "/agent/special/muse/triggers", access: "deny-remote", src: "routes/special.ts", why: "creates persistent follow-up execution (design appendix C)" },
|
|
326
|
+
{ method: "DELETE", path: "/agent/special/muse/triggers/:id", access: "deny-remote", src: "routes/special.ts", why: "Muse trigger config" },
|
|
327
|
+
{ method: "GET", path: "/agent/special/schedule", access: "allow", src: "routes/special.ts", why: "Agent schedules" },
|
|
328
|
+
{ method: "POST", path: "/agent/special/schedule/:slug/entries", access: "deny-remote", src: "routes/special.ts", why: "creates persistent follow-up execution (design appendix C)" },
|
|
329
|
+
{ method: "DELETE", path: "/agent/special/schedule/:slug/entries/:id", access: "deny-remote", src: "routes/special.ts", why: "schedule write" },
|
|
330
|
+
{ method: "GET", path: "/agent/teams", access: "allow", src: "routes/teams.ts", why: "team list" },
|
|
331
|
+
{ method: "POST", path: "/agent/teams", access: "deny-remote", src: "routes/teams.ts", why: "create team" },
|
|
332
|
+
{ method: "GET", path: "/agent/teams-meta", access: "allow", src: "routes/teams.ts", why: "team meta" },
|
|
333
|
+
{ method: "PUT", path: "/agent/teams-meta", access: "deny-remote", src: "routes/teams.ts", why: "team meta write" },
|
|
334
|
+
{ method: "DELETE", path: "/agent/teams/:slug", access: "deny-remote", src: "routes/teams.ts", why: "delete team" },
|
|
335
|
+
{ method: "GET", path: "/agent/teams/:slug", access: "allow", src: "routes/teams.ts", why: "team detail" },
|
|
336
|
+
{ method: "PATCH", path: "/agent/teams/:slug", access: "deny-remote", src: "routes/teams.ts", why: "update team" },
|
|
337
|
+
{ method: "DELETE", path: "/agent/teams/:slug/avatar", access: "deny-remote", src: "routes/teams.ts", why: "update team avatar" },
|
|
338
|
+
{ method: "GET", path: "/agent/teams/:slug/avatar", access: "allow", src: "routes/teams.ts", why: "team avatar" },
|
|
339
|
+
{ method: "POST", path: "/agent/teams/:slug/avatar", access: "deny-remote", src: "routes/teams.ts", why: "update team avatar" },
|
|
340
|
+
{ method: "POST", path: "/agent/teams/:slug/session/open", access: "allow", src: "routes/teams.ts", why: "open a team session" },
|
|
341
|
+
{ method: "POST", path: "/agent/tts", access: "allow", src: "routes/tts.ts", why: "read aloud" },
|
|
342
|
+
{ method: "POST", path: "/agent/tts/voices/clone", access: "deny-remote", src: "routes/tts.ts", why: "creates a provider voice" },
|
|
343
|
+
{ method: "POST", path: "/agent/tts/voices/delete", access: "deny-remote", src: "routes/tts.ts", why: "deletes a provider voice" },
|
|
344
|
+
{ method: "POST", path: "/agent/tts/voices/design", access: "deny-remote", src: "routes/tts.ts", why: "creates a provider voice" },
|
|
345
|
+
{ method: "POST", path: "/agent/tts/voices/list", access: "deny-remote", src: "routes/tts.ts", why: "posts to a caller-chosen baseUrl from the host network and echoes the reply (SSRF pivot into the host LAN)" },
|
|
346
|
+
{ method: "POST", path: "/agent/vision/describe", access: "allow", src: "routes/vision.ts", why: "describe an image for a non-vision model" },
|
|
347
|
+
{ method: "POST", path: "/agent/wechat/connect", access: "deny-remote", src: "routes/wechat.ts", why: "WeChat binding is local-only" },
|
|
348
|
+
{ method: "POST", path: "/agent/wechat/disconnect", access: "deny-remote", src: "routes/wechat.ts", why: "WeChat binding is local-only" },
|
|
349
|
+
{ method: "POST", path: "/agent/wechat/login/start", access: "deny-remote", src: "routes/wechat.ts", why: "WeChat binding is local-only" },
|
|
350
|
+
{ method: "GET", path: "/agent/wechat/login/status", access: "deny-remote", src: "routes/wechat.ts", why: "WeChat binding is local-only" },
|
|
351
|
+
{ method: "POST", path: "/agent/wechat/session-agent", access: "deny-remote", src: "routes/wechat.ts", why: "WeChat binding is local-only" },
|
|
352
|
+
{ method: "GET", path: "/agent/wechat/sessions", access: "deny-remote", src: "routes/wechat.ts", why: "WeChat binding is local-only" },
|
|
353
|
+
{ method: "POST", path: "/agent/wechat/sessions/new", access: "deny-remote", src: "routes/wechat.ts", why: "WeChat binding is local-only" },
|
|
354
|
+
{ method: "GET", path: "/agent/wechat/status", access: "deny-remote", src: "routes/wechat.ts", why: "WeChat binding is local-only" },
|
|
355
|
+
{ method: "GET", path: "/agent/projects", access: "allow", src: "routes/workspace.ts", why: "list cloud workspace projects" },
|
|
356
|
+
{ method: "POST", path: "/agent/projects", access: "deny-remote", src: "routes/workspace.ts", why: "create cloud workspace project (design appendix C)" },
|
|
357
|
+
{ method: "POST", path: "/agent/workspace/delete", access: "deny-remote", src: "routes/workspace.ts", why: "destructive workspace delete (design appendix C)" },
|
|
358
|
+
{ method: "GET", path: "/agent/workspace/download", access: "allow", src: "routes/workspace.ts", why: "download artifacts" },
|
|
359
|
+
{ method: "GET", path: "/agent/workspace/list", access: "allow", src: "routes/workspace.ts", why: "session workspace listing" },
|
|
360
|
+
{ method: "GET", path: "/agent/workspace/read", access: "allow", src: "routes/workspace.ts", why: "session workspace read" },
|
|
361
|
+
{ method: "POST", path: "/agent/workspace/upload", access: "allow", src: "routes/workspace.ts", why: "attachments from the remote device (design \xA76.6)" },
|
|
362
|
+
{ method: "GET", path: "/health", access: "allow", src: "standalone/main.ts", why: "unauthenticated liveness probe" }
|
|
363
|
+
];
|
|
364
|
+
|
|
365
|
+
// desktop/electron/unitCaller.ts
|
|
366
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
367
|
+
var UNIT_CALLER_HEADER = "x-unit-caller";
|
|
368
|
+
var ENGINE_CALLER_HEADER = "x-forsion-remote-caller";
|
|
369
|
+
var ASSERTION_WINDOW_MS = 6e4;
|
|
370
|
+
var SEEN_TTL_MS = 12e4;
|
|
371
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
372
|
+
var KINDS = /* @__PURE__ */ new Set(["phone", "desktop"]);
|
|
373
|
+
var UNSAFE_CHARS = /[\u0000-\u001f\u007f-\u009f\u200b-\u200f\u2028-\u202e\u2060-\u2069\ufeff]/g;
|
|
374
|
+
var cleanText = (v, max) => typeof v === "string" ? v.replace(UNSAFE_CHARS, "").trim().slice(0, max) : null;
|
|
375
|
+
function sanitizeProxyCaller(raw) {
|
|
376
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
377
|
+
const r = raw;
|
|
378
|
+
if (typeof r.unit !== "string" || !UUID_RE.test(r.unit)) return null;
|
|
379
|
+
if (typeof r.kind !== "string" || !KINDS.has(r.kind)) return null;
|
|
380
|
+
return {
|
|
381
|
+
unit: r.unit.toLowerCase(),
|
|
382
|
+
kind: r.kind,
|
|
383
|
+
name: cleanText(r.name, 120) ?? "",
|
|
384
|
+
platform: cleanText(r.platform, 40) || null,
|
|
385
|
+
registeredAt: cleanText(r.registeredAt, 40) || null
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
var b64 = (s) => Buffer.from(s, "utf8").toString("base64url");
|
|
389
|
+
var macOf = (key, body) => createHmac("sha256", key).update("v1." + body).digest();
|
|
390
|
+
function verifyProxyCaller(key, header, req, seen, nowMs = Date.now()) {
|
|
391
|
+
if (!key || typeof header !== "string" || header.length > 8192) return { ok: false, reason: "malformed" };
|
|
392
|
+
const m = /^v1\.([A-Za-z0-9_-]+)\.([A-Za-z0-9_-]+)$/.exec(header);
|
|
393
|
+
if (!m) return { ok: false, reason: "malformed" };
|
|
394
|
+
const given = Buffer.from(m[2], "base64url");
|
|
395
|
+
const want = macOf(key, m[1]);
|
|
396
|
+
if (given.length !== want.length || !timingSafeEqual(given, want)) return { ok: false, reason: "bad-mac" };
|
|
397
|
+
let p;
|
|
398
|
+
try {
|
|
399
|
+
p = JSON.parse(Buffer.from(m[1], "base64url").toString("utf8"));
|
|
400
|
+
} catch {
|
|
401
|
+
return { ok: false, reason: "malformed" };
|
|
402
|
+
}
|
|
403
|
+
if (!p || typeof p !== "object" || Array.isArray(p)) return { ok: false, reason: "malformed" };
|
|
404
|
+
if (p.pur !== "proxy") return { ok: false, reason: "purpose" };
|
|
405
|
+
if (p.m !== req.method) return { ok: false, reason: "method" };
|
|
406
|
+
if (p.t !== req.url) return { ok: false, reason: "target" };
|
|
407
|
+
if (typeof p.iat !== "number" || !Number.isFinite(p.iat) || Math.abs(nowMs - p.iat) > ASSERTION_WINDOW_MS) return { ok: false, reason: "stale" };
|
|
408
|
+
const d = p.d;
|
|
409
|
+
if (typeof d !== "string" || !d || d.length > 128) return { ok: false, reason: "malformed" };
|
|
410
|
+
const caller = sanitizeProxyCaller(p.c);
|
|
411
|
+
if (!caller) return { ok: false, reason: "malformed" };
|
|
412
|
+
const exp = seen.get(d);
|
|
413
|
+
if (exp !== void 0 && exp > nowMs) return { ok: false, reason: "replay" };
|
|
414
|
+
seen.set(d, nowMs + SEEN_TTL_MS);
|
|
415
|
+
return { ok: true, caller, dispatchId: d };
|
|
416
|
+
}
|
|
417
|
+
function gcSeenCallers(seen, nowMs = Date.now()) {
|
|
418
|
+
for (const [d, exp] of seen) if (exp <= nowMs) seen.delete(d);
|
|
419
|
+
}
|
|
420
|
+
function encodeEngineCaller(c) {
|
|
421
|
+
return b64(JSON.stringify({ u: c.unit, k: c.kind, n: c.name, p: c.platform, r: c.registeredAt }));
|
|
422
|
+
}
|
|
423
|
+
function callerOf(via, pair, caller) {
|
|
424
|
+
if (via === "tunnel") return caller ? { kind: "unit", caller } : { kind: "account" };
|
|
425
|
+
if (via === "p2p") return { kind: "p2p" };
|
|
426
|
+
return { kind: "paired", pairId: pair?.pairId ?? "", name: pair?.name ?? "" };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// desktop/shared/remoteSessions.ts
|
|
430
|
+
var REMOTE_SESSIONS_OFF = "REMOTE_SESSIONS_OFF";
|
|
431
|
+
var TRUST_REASON_SET = {
|
|
432
|
+
strict: true,
|
|
433
|
+
"never-prompts": true,
|
|
434
|
+
"not-signed-in": true,
|
|
435
|
+
"roster-miss": true,
|
|
436
|
+
"roster-unreachable": true,
|
|
437
|
+
"no-answer": true,
|
|
438
|
+
busy: true
|
|
439
|
+
};
|
|
440
|
+
var TRUST_REASONS = Object.keys(TRUST_REASON_SET);
|
|
441
|
+
var REMOTE_PROMPT_TTL_MS = 2 * 6e4;
|
|
442
|
+
|
|
443
|
+
// desktop/electron/remoteSessionGate.ts
|
|
444
|
+
var REMOTE_BASE_TIER_NON_GET = [
|
|
445
|
+
["POST", "/agent/runs/:runId/approvals/:approvalId"],
|
|
446
|
+
// 答审批(D1 / D2)
|
|
447
|
+
["POST", "/agent/runs/:runId/inquiries/:inquiryId"],
|
|
448
|
+
// 答询问(D1)
|
|
449
|
+
["POST", "/agent/special/approvals/:id/approve"],
|
|
450
|
+
// 异步审批(D2)
|
|
451
|
+
["POST", "/agent/special/approvals/:id/reject"],
|
|
452
|
+
["POST", "/agent/runs/:id/abort"],
|
|
453
|
+
// 远端停止(§6.1 急停「远端停止额外允许」)
|
|
454
|
+
// Agent Desk 截图回传:回应本机 run 的 desk_screenshot,shotId 随机且绑 run(P0 ②),与答审批同类;
|
|
455
|
+
// 放进会话档会让开关关时看着本机 run 的设备页把这个工具拖到超时。
|
|
456
|
+
["POST", "/agent/runs/:runId/captures/:shotId"],
|
|
457
|
+
// 不起执行、不建 / 改会话的杂务(K4 §8 设计文档修正 1;开关关时设备页的收件箱照常可用):
|
|
458
|
+
["PATCH", "/agent/inbox/:id"],
|
|
459
|
+
["DELETE", "/agent/inbox/:id"],
|
|
460
|
+
["POST", "/agent/inbox/:id/claim"],
|
|
461
|
+
["POST", "/agent/inbox/pull"],
|
|
462
|
+
["POST", "/agent/inbox/read-all"],
|
|
463
|
+
["PATCH", "/agent/special/muse/todos/:id"],
|
|
464
|
+
// 只改卡片状态(落点 / 忽略);`…/approve` 起自主执行,在会话档
|
|
465
|
+
["POST", "/agent/reply-segments"],
|
|
466
|
+
// 只读辅助
|
|
467
|
+
["POST", "/agent/commands/:name/expand"]
|
|
468
|
+
];
|
|
469
|
+
var escapeRe = (x) => x.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
470
|
+
var compile = (path) => new RegExp("^" + path.split("/").map((seg) => seg.startsWith(":") ? "[^/]+" : escapeRe(seg.toLowerCase())).join("/") + "$");
|
|
471
|
+
var BASE_KEYS = new Set(REMOTE_BASE_TIER_NON_GET.map(([m, p]) => `${m} ${p.toLowerCase()}`));
|
|
472
|
+
var ALLOW_ROWS = ENGINE_ROUTES.filter((r) => r.access === "allow").map((r) => ({
|
|
473
|
+
method: r.method,
|
|
474
|
+
base: BASE_KEYS.has(`${r.method} ${r.path.toLowerCase()}`),
|
|
475
|
+
re: compile(r.path)
|
|
476
|
+
}));
|
|
477
|
+
function remoteEngineTier(method, normPath) {
|
|
478
|
+
const m = method.toUpperCase() === "HEAD" ? "GET" : method.toUpperCase();
|
|
479
|
+
if (m === "GET") return "base";
|
|
480
|
+
const p = normPath.toLowerCase();
|
|
481
|
+
let hit = false;
|
|
482
|
+
for (const r of ALLOW_ROWS) {
|
|
483
|
+
if (r.method !== m || !r.re.test(p)) continue;
|
|
484
|
+
if (!r.base) return "session";
|
|
485
|
+
hit = true;
|
|
486
|
+
}
|
|
487
|
+
return hit ? "base" : "session";
|
|
488
|
+
}
|
|
489
|
+
var REMOTE_SESSIONS_OFF_DETAIL = "Remote sessions are turned off on this device";
|
|
490
|
+
var offResult = () => ({ ok: false, status: 403, body: { code: REMOTE_SESSIONS_OFF, detail: REMOTE_SESSIONS_OFF_DETAIL } });
|
|
491
|
+
function baseTierOnly(method, normPath) {
|
|
492
|
+
return remoteEngineTier(method, normPath) === "base" ? { ok: true } : offResult();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// desktop/shared/remoteSafety.ts
|
|
496
|
+
var REMOTE_LOCKED = "REMOTE_LOCKED";
|
|
497
|
+
|
|
498
|
+
// desktop/electron/remoteLockGate.ts
|
|
499
|
+
var REMOTE_LOCKED_BODY = { code: REMOTE_LOCKED, detail: "Remote access to this computer is locked. Unlock it on the computer itself." };
|
|
500
|
+
var READ_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
501
|
+
var ABORT_PATH = /^\/agent\/runs\/[^/]+\/abort$/i;
|
|
502
|
+
function lockedRequestAllowed(method, path) {
|
|
503
|
+
const m = String(method || "GET").toUpperCase();
|
|
504
|
+
if (READ_METHODS.has(m)) return true;
|
|
505
|
+
return m === "POST" && path === "/vault/asset-token";
|
|
506
|
+
}
|
|
507
|
+
function lockedEngineAllowed(method, normPath) {
|
|
508
|
+
const m = String(method || "GET").toUpperCase();
|
|
509
|
+
if (READ_METHODS.has(m)) return true;
|
|
510
|
+
return m === "POST" && ABORT_PATH.test(normPath);
|
|
511
|
+
}
|
|
512
|
+
var VAULT_RPC_READ = /* @__PURE__ */ new Set([
|
|
513
|
+
IPC.listPages,
|
|
514
|
+
IPC.listFiles,
|
|
515
|
+
IPC.readPage,
|
|
516
|
+
IPC.readVaultBytes,
|
|
517
|
+
IPC.search,
|
|
518
|
+
IPC.backlinks,
|
|
519
|
+
IPC.exclusiveAssets,
|
|
520
|
+
IPC.listTags,
|
|
521
|
+
IPC.pagesByTag,
|
|
522
|
+
IPC.resolveEmbed,
|
|
523
|
+
IPC.blockBacklinks,
|
|
524
|
+
IPC.listFolders,
|
|
525
|
+
IPC.listTrash,
|
|
526
|
+
IPC.pageIcons,
|
|
527
|
+
IPC.dbRead,
|
|
528
|
+
IPC.drawingRead,
|
|
529
|
+
IPC.readTextFile,
|
|
530
|
+
IPC.listPageProps,
|
|
531
|
+
IPC.pluginDataRead
|
|
532
|
+
]);
|
|
533
|
+
|
|
534
|
+
// desktop/electron/unitWeb.ts
|
|
535
|
+
var VAULT_RPC_ALLOW = /* @__PURE__ */ new Set([
|
|
536
|
+
IPC.restoreVault,
|
|
537
|
+
IPC.listPages,
|
|
538
|
+
IPC.listFiles,
|
|
539
|
+
IPC.loadPage,
|
|
540
|
+
IPC.readPage,
|
|
541
|
+
IPC.newPage,
|
|
542
|
+
IPC.savePage,
|
|
543
|
+
IPC.renamePage,
|
|
544
|
+
IPC.reconcilePage,
|
|
545
|
+
IPC.saveAsset,
|
|
546
|
+
IPC.saveVaultBytes,
|
|
547
|
+
IPC.readVaultBytes,
|
|
548
|
+
IPC.saveAttachment,
|
|
549
|
+
IPC.search,
|
|
550
|
+
IPC.backlinks,
|
|
551
|
+
IPC.exclusiveAssets,
|
|
552
|
+
IPC.reindex,
|
|
553
|
+
IPC.listTags,
|
|
554
|
+
IPC.pagesByTag,
|
|
555
|
+
IPC.deletePage,
|
|
556
|
+
IPC.movePage,
|
|
557
|
+
IPC.resolveEmbed,
|
|
558
|
+
IPC.blockBacklinks,
|
|
559
|
+
IPC.listFolders,
|
|
560
|
+
IPC.createFolder,
|
|
561
|
+
IPC.renameFolder,
|
|
562
|
+
IPC.deleteFolder,
|
|
563
|
+
IPC.moveFolder,
|
|
564
|
+
IPC.trashEntry,
|
|
565
|
+
IPC.listTrash,
|
|
566
|
+
IPC.restoreTrash,
|
|
567
|
+
IPC.deleteTrashEntry,
|
|
568
|
+
IPC.emptyTrash,
|
|
569
|
+
IPC.pageIcons,
|
|
570
|
+
IPC.fetchLinkMeta,
|
|
571
|
+
IPC.searchImages,
|
|
572
|
+
IPC.dbRead,
|
|
573
|
+
IPC.dbWrite,
|
|
574
|
+
IPC.dbWriteCas,
|
|
575
|
+
IPC.drawingRead,
|
|
576
|
+
IPC.drawingWrite,
|
|
577
|
+
IPC.readTextFile,
|
|
578
|
+
IPC.writeTextFile,
|
|
579
|
+
IPC.listPageProps,
|
|
580
|
+
IPC.setPageFrontmatter,
|
|
581
|
+
IPC.renamePageFile,
|
|
582
|
+
IPC.renameDbFile,
|
|
583
|
+
IPC.pluginDataRead,
|
|
584
|
+
IPC.pluginDataWrite,
|
|
585
|
+
IPC.patchMark
|
|
586
|
+
]);
|
|
587
|
+
var VAULT_RPC_LOCAL_ONLY = /* @__PURE__ */ new Set([IPC.emptyTrash, IPC.deleteTrashEntry]);
|
|
588
|
+
var UNIT_INTERNAL_HEADER = "x-unit-internal";
|
|
589
|
+
var UNIT_P2P_HEADER = "x-unit-p2p";
|
|
590
|
+
var LOCAL_ONLY_BODY = { code: "LOCAL_ONLY", detail: "This action is only available on the device itself" };
|
|
591
|
+
var BAD_CALLER_BODY = { code: "BAD_CALLER_ASSERTION", detail: "Invalid caller assertion" };
|
|
592
|
+
var ENGINE_PATH_CHARS = /^(?:[A-Za-z0-9\-._~!$&'()*+,;=:@/]|%[0-9A-Fa-f]{2})*$/;
|
|
593
|
+
var ENGINE_QUERY_CHARS = /^[\x21-\x7e]*$/;
|
|
594
|
+
function normalizeEnginePath(raw) {
|
|
595
|
+
if (!ENGINE_PATH_CHARS.test(raw)) return null;
|
|
596
|
+
if (/%(?:2f|5c|2e)/i.test(raw)) return null;
|
|
597
|
+
const segs = raw.split("/").filter((x) => x !== "");
|
|
598
|
+
if (segs.some((x) => x === "." || x === "..")) return null;
|
|
599
|
+
return "/" + segs.join("/");
|
|
600
|
+
}
|
|
601
|
+
function engineTarget(rawAfterEngine) {
|
|
602
|
+
if (rawAfterEngine.includes("#")) return null;
|
|
603
|
+
const q = rawAfterEngine.indexOf("?");
|
|
604
|
+
const rawPath = q >= 0 ? rawAfterEngine.slice(0, q) : rawAfterEngine;
|
|
605
|
+
const query = q >= 0 ? rawAfterEngine.slice(q) : "";
|
|
606
|
+
if (query && !ENGINE_QUERY_CHARS.test(query.slice(1))) return null;
|
|
607
|
+
const path = normalizeEnginePath(rawPath);
|
|
608
|
+
return path ? { path, query } : null;
|
|
609
|
+
}
|
|
610
|
+
var escapeRe2 = (x) => x.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
611
|
+
var COMPILED_ROUTES = ENGINE_ROUTES.map((r) => ({
|
|
612
|
+
method: r.method,
|
|
613
|
+
access: r.access,
|
|
614
|
+
re: new RegExp("^" + r.path.split("/").map((seg) => seg.startsWith(":") ? "[^/]+" : escapeRe2(seg.toLowerCase())).join("/") + "$")
|
|
615
|
+
}));
|
|
616
|
+
function engineRouteAccess(method, normPath) {
|
|
617
|
+
const m = method.toUpperCase() === "HEAD" ? "GET" : method.toUpperCase();
|
|
618
|
+
const p = normPath.toLowerCase();
|
|
619
|
+
let allowed = false;
|
|
620
|
+
for (const r of COMPILED_ROUTES) {
|
|
621
|
+
if (r.method !== m || !r.re.test(p)) continue;
|
|
622
|
+
if (r.access !== "allow") return "deny";
|
|
623
|
+
allowed = true;
|
|
624
|
+
}
|
|
625
|
+
return allowed ? "allow" : "unknown";
|
|
626
|
+
}
|
|
627
|
+
var decodeRpcArgs = (args2) => args2.map((a) => a && typeof a === "object" && typeof a.__u8 === "string" ? Buffer.from(a.__u8, "base64") : a);
|
|
628
|
+
var encodeRpcResult = (r) => r instanceof Uint8Array ? { __u8: Buffer.from(r).toString("base64") } : r;
|
|
629
|
+
var sha256 = (s) => createHash("sha256").update(s).digest("hex");
|
|
630
|
+
var PAIR_TTL_MS = 2 * 6e4;
|
|
631
|
+
var MIME = {
|
|
632
|
+
".html": "text/html; charset=utf-8",
|
|
633
|
+
".js": "text/javascript",
|
|
634
|
+
".mjs": "text/javascript",
|
|
635
|
+
".css": "text/css",
|
|
636
|
+
".svg": "image/svg+xml",
|
|
637
|
+
".png": "image/png",
|
|
638
|
+
".jpg": "image/jpeg",
|
|
639
|
+
".ico": "image/x-icon",
|
|
640
|
+
".json": "application/json",
|
|
641
|
+
".map": "application/json",
|
|
642
|
+
".woff2": "font/woff2",
|
|
643
|
+
".woff": "font/woff",
|
|
644
|
+
".ttf": "font/ttf",
|
|
645
|
+
".wasm": "application/wasm",
|
|
646
|
+
".webp": "image/webp",
|
|
647
|
+
".txt": "text/plain; charset=utf-8",
|
|
648
|
+
// vault 资源面(/vault/asset)常见附件类型
|
|
649
|
+
".gif": "image/gif",
|
|
650
|
+
".jpeg": "image/jpeg",
|
|
651
|
+
".pdf": "application/pdf",
|
|
652
|
+
".mp4": "video/mp4",
|
|
653
|
+
".webm": "video/webm",
|
|
654
|
+
".mov": "video/quicktime",
|
|
655
|
+
".mp3": "audio/mpeg",
|
|
656
|
+
".m4a": "audio/mp4",
|
|
657
|
+
".wav": "audio/wav",
|
|
658
|
+
".ogg": "audio/ogg",
|
|
659
|
+
".md": "text/markdown; charset=utf-8"
|
|
660
|
+
};
|
|
661
|
+
var HOST_DOWNLOAD_MAX_BYTES = 256 * 1024 * 1024;
|
|
662
|
+
var DOWNLOAD_MIME = {
|
|
663
|
+
...MIME,
|
|
664
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
665
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
666
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
667
|
+
".doc": "application/msword",
|
|
668
|
+
".xls": "application/vnd.ms-excel",
|
|
669
|
+
".ppt": "application/vnd.ms-powerpoint",
|
|
670
|
+
".csv": "text/csv; charset=utf-8",
|
|
671
|
+
".zip": "application/zip",
|
|
672
|
+
".html": "text/html; charset=utf-8",
|
|
673
|
+
".htm": "text/html; charset=utf-8"
|
|
674
|
+
};
|
|
675
|
+
function attachmentDisposition(name) {
|
|
676
|
+
const safe = name.replace(/[\r\n]/g, "") || "download";
|
|
677
|
+
const ascii = safe.replace(/[^\x20-\x7e]|["\\]/g, "_");
|
|
678
|
+
const utf8 = encodeURIComponent(safe).replace(/['()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
679
|
+
return `attachment; filename="${ascii}"; filename*=UTF-8''${utf8}`;
|
|
680
|
+
}
|
|
681
|
+
var isLoopback = (addr) => addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|
|
682
|
+
var secretEq = (got, want) => {
|
|
683
|
+
if (typeof got !== "string" || !want) return false;
|
|
684
|
+
const a = Buffer.from(got);
|
|
685
|
+
const b = Buffer.from(want);
|
|
686
|
+
return a.length === b.length && timingSafeEqual2(a, b);
|
|
687
|
+
};
|
|
688
|
+
var PLACEHOLDER = `<!doctype html><meta charset="utf-8"><title>Forsion Unit</title>
|
|
689
|
+
<body style="font-family:system-ui;max-width:560px;margin:80px auto;line-height:1.7">
|
|
690
|
+
<h2>\u672C\u673A\u672A\u6346 web \u6784\u5EFA</h2>
|
|
691
|
+
<p>\u8FD9\u53F0 Forsion \u7684\u4E92\u8054\u670D\u52A1\u5DF2\u5728\u8FD0\u884C,\u4F46\u7F3A\u5C11\u7F51\u9875\u6E32\u67D3\u5C42\u6784\u5EFA\u4EA7\u7269\u3002</p>
|
|
692
|
+
<p>\u5728\u8FD9\u53F0\u8BBE\u5907\u4E0A\u6267\u884C:</p>
|
|
693
|
+
<pre style="background:#f4f4f4;padding:12px;border-radius:8px">cd Forsion-Genesis/desktop && node scripts/build-unit-web.mjs</pre>
|
|
694
|
+
<p>\u6216\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF <code>TANGU_UNIT_WEB_DIST</code> \u6307\u5411\u5DF2\u6709\u7684 web \u6784\u5EFA\u76EE\u5F55\u540E\u91CD\u542F Forsion\u3002</p></body>`;
|
|
695
|
+
function startUnitWeb(deps, opts) {
|
|
696
|
+
if (deps.projection && !/^\/(?:[a-zA-Z0-9_-]+\/)*$/.test(deps.projection.basePath)) {
|
|
697
|
+
throw new Error("Projection basePath must be an absolute path ending in /");
|
|
698
|
+
}
|
|
699
|
+
const internalSecret = randomUUID();
|
|
700
|
+
const p2pSecret = randomBytes(32).toString("hex");
|
|
701
|
+
const proxyCallerKey = randomBytes(32).toString("hex");
|
|
702
|
+
const seenCallerDispatch = /* @__PURE__ */ new Map();
|
|
703
|
+
const ownerProjection = deps.projection?.mode === "local";
|
|
704
|
+
const pending = /* @__PURE__ */ new Map();
|
|
705
|
+
const pendingByIp = /* @__PURE__ */ new Map();
|
|
706
|
+
const assetTokens = /* @__PURE__ */ new Map();
|
|
707
|
+
const ASSET_TTL_MS = 10 * 6e4;
|
|
708
|
+
const gc = () => {
|
|
709
|
+
const now = Date.now();
|
|
710
|
+
for (const [id, p] of pending) {
|
|
711
|
+
if (p.expires < now) {
|
|
712
|
+
pending.delete(id);
|
|
713
|
+
if (pendingByIp.get(p.ip) === id) pendingByIp.delete(p.ip);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
for (const [t, rec] of assetTokens) if (rec.exp < now) assetTokens.delete(t);
|
|
717
|
+
gcSeenCallers(seenCallerDispatch, now);
|
|
718
|
+
};
|
|
719
|
+
const authInfo = (req) => {
|
|
720
|
+
if (isLoopback(req.socket.remoteAddress)) {
|
|
721
|
+
if (secretEq(req.headers[UNIT_INTERNAL_HEADER], internalSecret)) return { ok: true, pairHash: null, via: "tunnel" };
|
|
722
|
+
if (secretEq(req.headers[UNIT_P2P_HEADER], p2pSecret)) return { ok: true, pairHash: null, via: "p2p" };
|
|
723
|
+
}
|
|
724
|
+
const m = /^Bearer (.+)$/.exec(String(req.headers.authorization || ""));
|
|
725
|
+
if (!m) return { ok: false, pairHash: null, via: null };
|
|
726
|
+
const h = sha256(m[1]);
|
|
727
|
+
const hit = deps.pairedDevices.list().some((d) => d.tokenHash === h);
|
|
728
|
+
return { ok: hit, pairHash: hit ? h : null, via: hit ? "lan" : null };
|
|
729
|
+
};
|
|
730
|
+
const authed = (req) => authInfo(req).ok;
|
|
731
|
+
const pairInfo = (info) => {
|
|
732
|
+
if (!info.pairHash) return null;
|
|
733
|
+
const d = deps.pairedDevices.list().find((x) => x.tokenHash === info.pairHash);
|
|
734
|
+
return d ? { pairId: d.id, name: d.name } : null;
|
|
735
|
+
};
|
|
736
|
+
const resolveCaller = (req, info) => {
|
|
737
|
+
if (info.via !== "tunnel") return { ok: true, caller: null };
|
|
738
|
+
const raw = req.headers[UNIT_CALLER_HEADER];
|
|
739
|
+
if (raw === void 0) return { ok: true, caller: null };
|
|
740
|
+
if (typeof raw !== "string") return { ok: false };
|
|
741
|
+
const r = verifyProxyCaller(proxyCallerKey, raw, { method: req.method || "GET", url: req.url || "" }, seenCallerDispatch);
|
|
742
|
+
if (!r.ok) {
|
|
743
|
+
deps.log(`[unit-web] \u8C03\u7528\u65B9\u65AD\u8A00\u65E0\u6548(${r.reason}),\u5DF2\u62D2\u7EDD`);
|
|
744
|
+
return { ok: false };
|
|
745
|
+
}
|
|
746
|
+
return { ok: true, caller: r.caller };
|
|
747
|
+
};
|
|
748
|
+
const assetTokenLive = (at) => {
|
|
749
|
+
const rec = assetTokens.get(at);
|
|
750
|
+
if (!rec || rec.exp < Date.now()) return false;
|
|
751
|
+
return rec.pairHash === null || deps.pairedDevices.list().some((d) => d.tokenHash === rec.pairHash);
|
|
752
|
+
};
|
|
753
|
+
const underVaultReal = async (abs, rootRaw) => {
|
|
754
|
+
if (!rootRaw) return null;
|
|
755
|
+
try {
|
|
756
|
+
const [real, rootReal] = await Promise.all([realpath(abs), realpath(rootRaw)]);
|
|
757
|
+
return real === rootReal || real.startsWith(rootReal + sep) ? real : null;
|
|
758
|
+
} catch {
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
const json = (res, status, body) => {
|
|
763
|
+
const buf = Buffer.from(JSON.stringify(body));
|
|
764
|
+
res.writeHead(status, { "Content-Type": "application/json", "Content-Length": buf.length });
|
|
765
|
+
res.end(buf);
|
|
766
|
+
};
|
|
767
|
+
const readBody = (req, limit = 64 * 1024) => new Promise((resolve8, reject) => {
|
|
768
|
+
let n = 0;
|
|
769
|
+
const chunks = [];
|
|
770
|
+
req.on("data", (c) => {
|
|
771
|
+
n += c.length;
|
|
772
|
+
if (n > limit) {
|
|
773
|
+
reject(new Error("body too large"));
|
|
774
|
+
req.destroy();
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
chunks.push(c);
|
|
778
|
+
});
|
|
779
|
+
req.on("end", () => resolve8(Buffer.concat(chunks).toString("utf8")));
|
|
780
|
+
req.on("error", reject);
|
|
781
|
+
});
|
|
782
|
+
const isLocalOnlyEnginePath = (path) => {
|
|
783
|
+
let p = path;
|
|
784
|
+
try {
|
|
785
|
+
p = decodeURIComponent(new URL(path, "http://x").pathname);
|
|
786
|
+
} catch {
|
|
787
|
+
}
|
|
788
|
+
return p.toLowerCase().replace(/\/{2,}/g, "/").startsWith("/engine/agent/browser-extension");
|
|
789
|
+
};
|
|
790
|
+
const proxyEngine = (req, res, path, via, caller) => {
|
|
791
|
+
const engine = deps.getEngine();
|
|
792
|
+
if (!engine.url) {
|
|
793
|
+
json(res, 503, { detail: "\u672C\u673A\u5F15\u64CE\u672A\u5C31\u7EEA", code: "ENGINE_NOT_READY" });
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
const target = new URL(engine.url);
|
|
797
|
+
const headers = { Authorization: `Bearer ${engine.token}` };
|
|
798
|
+
for (const k of ["content-type", "accept", "content-length"]) {
|
|
799
|
+
const v = req.headers[k];
|
|
800
|
+
if (typeof v === "string") headers[k] = v;
|
|
801
|
+
}
|
|
802
|
+
if (via) {
|
|
803
|
+
headers["x-forsion-remote"] = via;
|
|
804
|
+
if (engine.remoteMark) headers["x-forsion-remote-mark"] = engine.remoteMark;
|
|
805
|
+
if (via === "tunnel" && caller) headers[ENGINE_CALLER_HEADER] = encodeEngineCaller(caller);
|
|
806
|
+
}
|
|
807
|
+
const up = http.request({
|
|
808
|
+
host: target.hostname,
|
|
809
|
+
port: target.port,
|
|
810
|
+
path,
|
|
811
|
+
method: req.method,
|
|
812
|
+
headers
|
|
813
|
+
}, (upRes) => {
|
|
814
|
+
const h = {};
|
|
815
|
+
for (const k of ["content-type", "cache-control"]) {
|
|
816
|
+
const v = upRes.headers[k];
|
|
817
|
+
if (typeof v === "string") h[k] = v;
|
|
818
|
+
}
|
|
819
|
+
if (String(upRes.headers["content-type"] || "").includes("text/event-stream")) h["X-Accel-Buffering"] = "no";
|
|
820
|
+
res.writeHead(upRes.statusCode || 502, h);
|
|
821
|
+
upRes.pipe(res);
|
|
822
|
+
res.on("close", () => upRes.destroy());
|
|
823
|
+
});
|
|
824
|
+
up.on("error", (e) => {
|
|
825
|
+
if (!res.headersSent) json(res, 502, { detail: `\u672C\u673A\u5F15\u64CE\u4E0D\u53EF\u8FBE: ${e.message}` });
|
|
826
|
+
});
|
|
827
|
+
req.pipe(up);
|
|
828
|
+
};
|
|
829
|
+
const serveStatic = async (res, urlPath) => {
|
|
830
|
+
const dist2 = deps.webDistDir();
|
|
831
|
+
if (!dist2) {
|
|
832
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-cache" });
|
|
833
|
+
res.end(PLACEHOLDER);
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
let rel = decodeURIComponent(urlPath.split("?")[0]);
|
|
837
|
+
if (!extname(rel)) rel = "/index.html";
|
|
838
|
+
const norm = normalize(rel).replace(/^([/\\])+/, "");
|
|
839
|
+
if (norm.startsWith("..")) {
|
|
840
|
+
json(res, 400, { detail: "bad path" });
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
try {
|
|
844
|
+
const [rootReal, fileReal] = await Promise.all([realpath(dist2), realpath(`${dist2}/${norm}`)]);
|
|
845
|
+
if (!fileReal.startsWith(rootReal + sep)) {
|
|
846
|
+
json(res, 404, { detail: "not found" });
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
let buf = await readFile(fileReal);
|
|
850
|
+
const ext = extname(norm);
|
|
851
|
+
if (norm === "index.html") {
|
|
852
|
+
const encode = (value) => JSON.stringify(value).replace(/</g, "\\u003c");
|
|
853
|
+
const meta = { ...deps.meta, ...deps.projection ? { projection: deps.projection.mode, browserStorage: deps.projection.mode === "public", account: deps.account?.metadata(), capabilities: deps.projection.capabilities?.(), localCapabilities: deps.projection.mode === "local" ? deps.projection.localCapabilities?.() : void 0 } : {} };
|
|
854
|
+
const baseTag = deps.projection ? `<base href="${deps.projection.basePath}">` : "";
|
|
855
|
+
const inject = `${baseTag}<script>window.__FORSION_UNIT_PAGE__=${encode(meta)};window.__FORSION_PRODUCT_RUNTIME__=${encode(deps.projection?.product ?? PRODUCT)}</script>`;
|
|
856
|
+
let html = buf.toString("utf8").replace(/<head>/i, `<head>${inject}`);
|
|
857
|
+
html = html.replace(/<meta[^>]*http-equiv=["']Content-Security-Policy["'][^>]*>/i, (tag) => tag.replace(/(content=(["']))([\s\S]*?)\2/i, (_m, head, quote, csp) => {
|
|
858
|
+
const next = csp.includes("'unsafe-eval'") ? csp : csp.replace(/script-src([^;]*)/i, (_d, srcs) => `script-src${srcs} 'unsafe-eval'`);
|
|
859
|
+
return `${head}${next}${quote}`;
|
|
860
|
+
}));
|
|
861
|
+
buf = Buffer.from(html);
|
|
862
|
+
}
|
|
863
|
+
const h = { "Content-Type": MIME[ext] || "application/octet-stream", "Content-Length": buf.length };
|
|
864
|
+
if (norm === "index.html") h["Cache-Control"] = "no-cache";
|
|
865
|
+
else if (/^assets[\\/]/.test(norm)) h["Cache-Control"] = "public, max-age=31536000, immutable";
|
|
866
|
+
res.writeHead(200, h);
|
|
867
|
+
res.end(buf);
|
|
868
|
+
} catch {
|
|
869
|
+
json(res, 404, { detail: "not found" });
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
const hostFileRequest = (req, res, url) => {
|
|
873
|
+
const info = authInfo(req);
|
|
874
|
+
if (!info.ok) {
|
|
875
|
+
json(res, 401, { detail: "\u672A\u914D\u5BF9", code: "UNPAIRED" });
|
|
876
|
+
return null;
|
|
877
|
+
}
|
|
878
|
+
return { p: String(new URL(url, "http://x").searchParams.get("path") || ""), via: info.via };
|
|
879
|
+
};
|
|
880
|
+
const sendHostDownload = async (res, p) => {
|
|
881
|
+
if (!deps.openHostFile) {
|
|
882
|
+
json(res, 501, { detail: "Host file download is not available on this device", code: "HOST_DOWNLOAD_UNSUPPORTED" });
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
const f = await deps.openHostFile(p);
|
|
886
|
+
if (!f) {
|
|
887
|
+
json(res, 404, { detail: "not readable" });
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
if (f.size > HOST_DOWNLOAD_MAX_BYTES) {
|
|
891
|
+
await f.fh.close().catch(() => {
|
|
892
|
+
});
|
|
893
|
+
json(res, 413, { detail: `File is larger than the ${HOST_DOWNLOAD_MAX_BYTES / 1048576} MB remote download limit`, code: "HOST_DOWNLOAD_TOO_LARGE", size: f.size, limit: HOST_DOWNLOAD_MAX_BYTES });
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
const name = p.split(/[\\/]/).filter(Boolean).pop() || "download";
|
|
897
|
+
res.writeHead(200, {
|
|
898
|
+
"Content-Type": DOWNLOAD_MIME[extname(f.real).toLowerCase()] || "application/octet-stream",
|
|
899
|
+
"Content-Length": f.size,
|
|
900
|
+
"Content-Disposition": attachmentDisposition(name),
|
|
901
|
+
// 同 /vault/asset:主机文件是不受信内容。隧道的 /stream 面不转 Content-Disposition,有人把下载链接当页面直开时,
|
|
902
|
+
// HTML / SVG 会在鉴权过的 proxy origin 上执行 —— nosniff + CSP sandbox 惰化(网关宣告 stream-sec-headers 才走流式,见 unitHost.streamWorthy)。
|
|
903
|
+
"X-Content-Type-Options": "nosniff",
|
|
904
|
+
"Content-Security-Policy": "sandbox",
|
|
905
|
+
"Cache-Control": "no-store"
|
|
906
|
+
});
|
|
907
|
+
if (f.size === 0) {
|
|
908
|
+
await f.fh.close().catch(() => {
|
|
909
|
+
});
|
|
910
|
+
res.end();
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
const stream = f.fh.createReadStream({ start: 0, end: f.size - 1 });
|
|
914
|
+
stream.on("error", () => res.destroy());
|
|
915
|
+
res.on("close", () => stream.destroy());
|
|
916
|
+
stream.pipe(res);
|
|
917
|
+
};
|
|
918
|
+
const plainPath = (req) => {
|
|
919
|
+
let u = req.url || "/";
|
|
920
|
+
if (deps.projection) {
|
|
921
|
+
const prefix = deps.projection.basePath.replace(/\/$/, "");
|
|
922
|
+
if (u.startsWith(prefix + "/")) u = u.slice(prefix.length);
|
|
923
|
+
}
|
|
924
|
+
return u.split("?")[0];
|
|
925
|
+
};
|
|
926
|
+
const lockedNow = () => {
|
|
927
|
+
if (!deps.remoteLock) return false;
|
|
928
|
+
try {
|
|
929
|
+
return deps.remoteLock();
|
|
930
|
+
} catch {
|
|
931
|
+
return true;
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
const handler = async (req, res) => {
|
|
935
|
+
if (!ownerProjection && lockedNow()) {
|
|
936
|
+
const p = plainPath(req);
|
|
937
|
+
if (!(p === "/engine" || p.startsWith("/engine/")) && p !== "/vault/rpc" && !lockedRequestAllowed(req.method || "GET", p)) {
|
|
938
|
+
json(res, 423, REMOTE_LOCKED_BODY);
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
if (await deps.routeRequest?.(req, res)) return;
|
|
943
|
+
gc();
|
|
944
|
+
let url = req.url || "/";
|
|
945
|
+
if (deps.projection) {
|
|
946
|
+
const prefix = deps.projection.basePath.replace(/\/$/, "");
|
|
947
|
+
if (url === prefix || url.startsWith(prefix + "?")) {
|
|
948
|
+
res.writeHead(308, { Location: prefix + "/" + url.slice(prefix.length) });
|
|
949
|
+
res.end();
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
if (url.startsWith(prefix + "/")) url = url.slice(prefix.length);
|
|
953
|
+
}
|
|
954
|
+
const path = url.split("?")[0];
|
|
955
|
+
const ip = req.socket.remoteAddress || "unknown";
|
|
956
|
+
if (deps.projection?.mode === "public") {
|
|
957
|
+
if (await deps.account?.handle(path, req, res)) return;
|
|
958
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
959
|
+
json(res, 405, { detail: "Read-only projection" });
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
if (path === "/unit/whoami") {
|
|
963
|
+
json(res, 200, { ok: true, scope: "shell" });
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
if (path === "/unit/plugins") {
|
|
967
|
+
json(res, 200, { appVersion: deps.meta.version, plugins: await deps.readPlugins() });
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
if (path === "/unit/spaces") {
|
|
971
|
+
json(res, 200, { spaces: await deps.readSpaces() });
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
if (path === "/unit/config") {
|
|
975
|
+
json(res, 200, { config: {} });
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
if (path === "/unit/providers") {
|
|
979
|
+
json(res, 200, { providers: [] });
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
if (path === "/unit/meta") {
|
|
983
|
+
json(res, 200, { ...deps.meta, pair: false, projection: "public", account: deps.account?.metadata(), capabilities: deps.projection?.capabilities?.() });
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
if (/^\/(?:unit|vault|engine)(?:\/|$)/.test(path)) {
|
|
987
|
+
json(res, 403, { detail: "Host capability is not published" });
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
if (path === "/unit/meta" && req.method === "GET") {
|
|
992
|
+
json(res, 200, { ...deps.meta, pair: !deps.projection, ...deps.projection?.mode === "local" ? { projection: "local", localCapabilities: deps.projection.localCapabilities?.() } : {} });
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
if (deps.projection?.mode === "local" && path.startsWith("/unit/pair/")) {
|
|
996
|
+
json(res, 403, { detail: "Use the workspace owner access key" });
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
if (path === "/unit/pair/request" && req.method === "POST") {
|
|
1000
|
+
const prevId = pendingByIp.get(ip);
|
|
1001
|
+
if (prevId && pending.get(prevId)?.status === "pending") {
|
|
1002
|
+
json(res, 429, { detail: "\u5DF2\u6709\u5F85\u786E\u8BA4\u7684\u914D\u5BF9\u8BF7\u6C42", code: "PAIR_PENDING" });
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
let name = "\u672A\u547D\u540D\u8BBE\u5907";
|
|
1006
|
+
try {
|
|
1007
|
+
name = String(JSON.parse(await readBody(req))?.name || name).slice(0, 60);
|
|
1008
|
+
} catch {
|
|
1009
|
+
}
|
|
1010
|
+
const p = {
|
|
1011
|
+
id: randomUUID(),
|
|
1012
|
+
name,
|
|
1013
|
+
code: String(Math.floor(Math.random() * 1e6)).padStart(6, "0"),
|
|
1014
|
+
ip,
|
|
1015
|
+
expires: Date.now() + PAIR_TTL_MS,
|
|
1016
|
+
status: "pending"
|
|
1017
|
+
};
|
|
1018
|
+
pending.set(p.id, p);
|
|
1019
|
+
pendingByIp.set(ip, p.id);
|
|
1020
|
+
json(res, 200, { requestId: p.id, code: p.code, ttlMs: PAIR_TTL_MS });
|
|
1021
|
+
void deps.confirmPair({ name: p.name, code: p.code, ip }).then(async (ok) => {
|
|
1022
|
+
const live = pending.get(p.id);
|
|
1023
|
+
if (!live || live.status !== "pending" || live.expires < Date.now()) return;
|
|
1024
|
+
if (!ok) {
|
|
1025
|
+
live.status = "denied";
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
const token = randomBytes(32).toString("hex");
|
|
1029
|
+
await deps.pairedDevices.add({ id: randomUUID(), name: live.name, tokenHash: sha256(token), createdAt: Date.now() });
|
|
1030
|
+
live.status = "approved";
|
|
1031
|
+
live.token = token;
|
|
1032
|
+
deps.log(`[unit-web] \u5DF2\u914D\u5BF9\u8BBE\u5907\u300C${live.name}\u300D(${ip})`);
|
|
1033
|
+
}).catch((e) => deps.log(`[unit-web] \u914D\u5BF9\u786E\u8BA4\u5931\u8D25: ${e?.message || e}`));
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
if (path === "/unit/pair/poll" && req.method === "GET") {
|
|
1037
|
+
const id = String(new URL(url, "http://x").searchParams.get("id") || "");
|
|
1038
|
+
const p = pending.get(id);
|
|
1039
|
+
if (!p || p.expires < Date.now()) {
|
|
1040
|
+
json(res, 200, { status: "expired" });
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
if (p.status === "approved" && p.token) {
|
|
1044
|
+
const token = p.token;
|
|
1045
|
+
pending.delete(id);
|
|
1046
|
+
pendingByIp.delete(p.ip);
|
|
1047
|
+
json(res, 200, { status: "approved", token });
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
json(res, 200, { status: p.status });
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
if (path === "/unit/whoami" && req.method === "GET") {
|
|
1054
|
+
if (!authed(req)) {
|
|
1055
|
+
json(res, 401, { detail: "\u672A\u914D\u5BF9", code: "UNPAIRED" });
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
json(res, 200, { ok: true });
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
if (path === "/unit/p2p/offer" && req.method === "POST") {
|
|
1062
|
+
if (!authed(req)) {
|
|
1063
|
+
json(res, 401, { detail: "\u672A\u914D\u5BF9", code: "UNPAIRED" });
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
if (!deps.p2pAnswer) {
|
|
1067
|
+
json(res, 501, { detail: "\u672C\u7AEF\u4E0D\u652F\u6301 P2P \u76F4\u8FDE", code: "P2P_UNSUPPORTED" });
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
let offer = "";
|
|
1071
|
+
try {
|
|
1072
|
+
offer = String(JSON.parse(await readBody(req))?.sdp || "");
|
|
1073
|
+
} catch {
|
|
1074
|
+
}
|
|
1075
|
+
if (!offer) {
|
|
1076
|
+
json(res, 400, { detail: "\u7F3A offer SDP" });
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
try {
|
|
1080
|
+
json(res, 200, { sdp: await deps.p2pAnswer(offer) });
|
|
1081
|
+
} catch (e) {
|
|
1082
|
+
json(res, 500, { detail: `P2P \u5E94\u7B54\u5931\u8D25: ${e?.message || e}` });
|
|
1083
|
+
}
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
if (path === "/unit/plugins" && req.method === "GET") {
|
|
1087
|
+
if (!authed(req)) {
|
|
1088
|
+
json(res, 401, { detail: "\u672A\u914D\u5BF9", code: "UNPAIRED" });
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
json(res, 200, { appVersion: deps.meta.version, plugins: await deps.readPlugins() });
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
if (path === "/unit/spaces" && req.method === "GET") {
|
|
1095
|
+
if (!authed(req)) {
|
|
1096
|
+
json(res, 401, { detail: "\u672A\u914D\u5BF9", code: "UNPAIRED" });
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
json(res, 200, { spaces: await deps.readSpaces() });
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
if (path === "/unit/providers" && req.method === "GET") {
|
|
1103
|
+
if (!authed(req)) {
|
|
1104
|
+
json(res, 401, { detail: "\u672A\u914D\u5BF9", code: "UNPAIRED" });
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
json(res, 200, { providers: await deps.readProviders() });
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
if ((path === "/unit/hostfile" || path === "/unit/hostfile/download") && req.method === "GET") {
|
|
1111
|
+
const q = hostFileRequest(req, res, url);
|
|
1112
|
+
if (!q) return;
|
|
1113
|
+
if (path === "/unit/hostfile/download") {
|
|
1114
|
+
await sendHostDownload(res, q.p);
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
const f = await deps.readHostFile(q.p, q.via === "tunnel" ? 4 * 1024 * 1024 : void 0);
|
|
1118
|
+
if (!f) {
|
|
1119
|
+
json(res, 404, { detail: "not readable" });
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
json(res, 200, f);
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
if ((path === "/unit/hostdir" || path === "/unit/hoststat") && req.method === "GET") {
|
|
1126
|
+
if (!authed(req)) {
|
|
1127
|
+
json(res, 401, { detail: "\u672A\u914D\u5BF9", code: "UNPAIRED" });
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
const p = String(new URL(url, "http://x").searchParams.get("path") || "");
|
|
1131
|
+
const r = path === "/unit/hostdir" ? await deps.readHostDir(p) : await deps.readHostStat(p);
|
|
1132
|
+
if (!r) {
|
|
1133
|
+
json(res, 404, { detail: "not readable" });
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
json(res, 200, path === "/unit/hostdir" ? { entries: r } : r);
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
if (path === "/unit/config" && (req.method === "GET" || req.method === "PUT")) {
|
|
1140
|
+
if (!authed(req)) {
|
|
1141
|
+
json(res, 401, { detail: "\u672A\u914D\u5BF9", code: "UNPAIRED" });
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
if (req.method === "GET") {
|
|
1145
|
+
json(res, 200, { config: await deps.readConfig() });
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
let patch;
|
|
1149
|
+
try {
|
|
1150
|
+
patch = JSON.parse(await readBody(req));
|
|
1151
|
+
} catch {
|
|
1152
|
+
json(res, 400, { detail: "bad body" });
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
json(res, 200, { config: await deps.writeConfig(patch && typeof patch === "object" ? patch : {}) });
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
if (path === "/unit/remote-access" && req.method === "GET" || path === "/unit/remote-access/request" && req.method === "POST") {
|
|
1159
|
+
const info = authInfo(req);
|
|
1160
|
+
if (!info.ok) {
|
|
1161
|
+
json(res, 401, { detail: "Not paired", code: "UNPAIRED" });
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
const rc = resolveCaller(req, info);
|
|
1165
|
+
if (!rc.ok) {
|
|
1166
|
+
json(res, 403, BAD_CALLER_BODY);
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
const caller = callerOf(info.via, pairInfo(info), rc.caller);
|
|
1170
|
+
const ra = deps.remoteAccess;
|
|
1171
|
+
if (!ra) {
|
|
1172
|
+
json(res, 200, { remoteSessions: false, principal: caller.kind === "paired" ? "lan" : caller.kind, caller: caller.kind === "paired" ? "paired" : "unconfirmed", maxApprovalMode: "auto-edit" });
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
json(res, 200, path === "/unit/remote-access" ? ra.status(caller) : await ra.request(caller));
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
if (path === "/engine" || path.startsWith("/engine/")) {
|
|
1179
|
+
const method = req.method || "GET";
|
|
1180
|
+
const info = authInfo(req);
|
|
1181
|
+
if (!info.ok) {
|
|
1182
|
+
json(res, 401, { detail: "\u672A\u914D\u5BF9", code: "UNPAIRED" });
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
if (isLocalOnlyEnginePath(url)) {
|
|
1186
|
+
if (!engineTarget(url.slice("/engine".length))) {
|
|
1187
|
+
json(res, 400, { detail: "Ambiguous engine path", code: "BAD_PATH" });
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
json(res, 403, LOCAL_ONLY_BODY);
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
if (ownerProjection) {
|
|
1194
|
+
proxyEngine(req, res, url.slice("/engine".length) || "/", null, null);
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
const target = engineTarget(url.slice("/engine".length));
|
|
1198
|
+
if (!target) {
|
|
1199
|
+
json(res, 400, { detail: "Ambiguous engine path", code: "BAD_PATH" });
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
if (engineRouteAccess(method, target.path) !== "allow") {
|
|
1203
|
+
json(res, 403, LOCAL_ONLY_BODY);
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
const rc = resolveCaller(req, info);
|
|
1207
|
+
if (!rc.ok) {
|
|
1208
|
+
json(res, 403, BAD_CALLER_BODY);
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
if (lockedNow() && !lockedEngineAllowed(method, target.path)) {
|
|
1212
|
+
json(res, 423, REMOTE_LOCKED_BODY);
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
const caller = callerOf(info.via, pairInfo(info), rc.caller);
|
|
1216
|
+
const g = deps.remoteAccess ? deps.remoteAccess.gateEngine({ method, path: target.path, via: info.via, caller }) : baseTierOnly(method, target.path);
|
|
1217
|
+
if (!g.ok) {
|
|
1218
|
+
json(res, g.status, g.body);
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
proxyEngine(req, res, target.path + target.query, info.via, rc.caller);
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
if (path.startsWith("/vault/")) {
|
|
1225
|
+
const vault = deps.vault();
|
|
1226
|
+
if (!vault) {
|
|
1227
|
+
json(res, 503, { detail: "\u672C\u673A\u667A\u5E93\u9762\u672A\u5C31\u7EEA", code: "VAULT_NOT_READY" });
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
const q = new URL(url, "http://x").searchParams;
|
|
1231
|
+
const at = String(q.get("at") || "");
|
|
1232
|
+
const assetAuthed = () => at !== "" && assetTokenLive(at) || authed(req);
|
|
1233
|
+
if (path === "/vault/rpc" && req.method === "POST") {
|
|
1234
|
+
if (!authed(req)) {
|
|
1235
|
+
json(res, 401, { detail: "\u672A\u914D\u5BF9", code: "UNPAIRED" });
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
let body;
|
|
1239
|
+
try {
|
|
1240
|
+
body = JSON.parse(await readBody(req, 32 * 1024 * 1024));
|
|
1241
|
+
} catch {
|
|
1242
|
+
json(res, 400, { detail: "bad body" });
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
const ch = String(body.ch || "");
|
|
1246
|
+
if (!VAULT_RPC_ALLOW.has(ch)) {
|
|
1247
|
+
json(res, 400, { detail: `\u901A\u9053\u4E0D\u53EF\u8FDC\u7A0B\u8C03\u7528: ${ch}`, code: "VAULT_CH_DENIED" });
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
if (!ownerProjection && VAULT_RPC_LOCAL_ONLY.has(ch)) {
|
|
1251
|
+
json(res, 403, LOCAL_ONLY_BODY);
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
if (!ownerProjection && lockedNow() && !VAULT_RPC_READ.has(ch)) {
|
|
1255
|
+
json(res, 423, REMOTE_LOCKED_BODY);
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
const origin = String(body.client || "").slice(0, 64) || null;
|
|
1259
|
+
try {
|
|
1260
|
+
const result = await vault.call(ch, decodeRpcArgs(Array.isArray(body.args) ? body.args : []), origin);
|
|
1261
|
+
json(res, 200, { ok: true, result: encodeRpcResult(result) ?? null });
|
|
1262
|
+
} catch (e) {
|
|
1263
|
+
json(res, 200, { ok: false, error: String(e?.message || e) });
|
|
1264
|
+
}
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
if (path === "/vault/asset-token" && req.method === "POST") {
|
|
1268
|
+
const info = authInfo(req);
|
|
1269
|
+
if (!info.ok) {
|
|
1270
|
+
json(res, 401, { detail: "\u672A\u914D\u5BF9", code: "UNPAIRED" });
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
const token = randomBytes(16).toString("hex");
|
|
1274
|
+
assetTokens.set(token, { exp: Date.now() + ASSET_TTL_MS, pairHash: info.pairHash });
|
|
1275
|
+
json(res, 200, { token, ttlSec: ASSET_TTL_MS / 1e3 });
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
if (path === "/vault/asset" && (req.method === "GET" || req.method === "HEAD")) {
|
|
1279
|
+
if (!assetAuthed()) {
|
|
1280
|
+
json(res, 401, { detail: "\u8D44\u6E90\u4EE4\u724C\u65E0\u6548", code: "ASSET_TOKEN" });
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
const exact = q.get("path");
|
|
1284
|
+
let abs = null;
|
|
1285
|
+
try {
|
|
1286
|
+
abs = exact != null ? vault.absPath(exact) : await vault.assetAbs(q.get("page"), String(q.get("ref") || ""));
|
|
1287
|
+
} catch {
|
|
1288
|
+
}
|
|
1289
|
+
const real = abs ? await underVaultReal(abs, vault.root()) : null;
|
|
1290
|
+
if (!real) {
|
|
1291
|
+
json(res, 404, { detail: "not found" });
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
const stream = createReadStream(real);
|
|
1295
|
+
stream.on("error", () => {
|
|
1296
|
+
if (!res.headersSent) json(res, 404, { detail: "not found" });
|
|
1297
|
+
else res.destroy();
|
|
1298
|
+
});
|
|
1299
|
+
stream.once("open", () => {
|
|
1300
|
+
const mime = MIME[extname(real).toLowerCase()] || "application/octet-stream";
|
|
1301
|
+
const h = { "Content-Type": mime, "X-Content-Type-Options": "nosniff" };
|
|
1302
|
+
if (mime !== "application/pdf") h["Content-Security-Policy"] = "sandbox";
|
|
1303
|
+
res.writeHead(200, h);
|
|
1304
|
+
stream.pipe(res);
|
|
1305
|
+
});
|
|
1306
|
+
res.on("close", () => stream.destroy());
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
if (path === "/vault/events" && req.method === "GET") {
|
|
1310
|
+
if (!assetAuthed()) {
|
|
1311
|
+
json(res, 401, { detail: "\u8D44\u6E90\u4EE4\u724C\u65E0\u6548", code: "ASSET_TOKEN" });
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
const viaAt = at !== "" && assetTokenLive(at);
|
|
1315
|
+
const bearerInfo = viaAt ? null : authInfo(req);
|
|
1316
|
+
const stillAuthed = () => viaAt ? assetTokenLive(at) : bearerInfo?.pairHash != null ? deps.pairedDevices.list().some((d) => d.tokenHash === bearerInfo.pairHash) : true;
|
|
1317
|
+
res.writeHead(200, {
|
|
1318
|
+
"Content-Type": "text/event-stream",
|
|
1319
|
+
"Cache-Control": "no-cache, no-transform",
|
|
1320
|
+
"X-Accel-Buffering": "no"
|
|
1321
|
+
});
|
|
1322
|
+
res.write(": connected\n\n");
|
|
1323
|
+
const unsub = vault.onEvent((ch, payload, origin) => {
|
|
1324
|
+
if (!stillAuthed()) {
|
|
1325
|
+
res.end();
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
res.write(`data: ${JSON.stringify({ ch, payload, origin })}
|
|
1329
|
+
|
|
1330
|
+
`);
|
|
1331
|
+
});
|
|
1332
|
+
const hb = setInterval(() => {
|
|
1333
|
+
if (!stillAuthed()) {
|
|
1334
|
+
res.end();
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
res.write(": hb\n\n");
|
|
1338
|
+
}, 25e3);
|
|
1339
|
+
res.on("close", () => {
|
|
1340
|
+
unsub();
|
|
1341
|
+
clearInterval(hb);
|
|
1342
|
+
});
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
json(res, 404, { detail: "not found" });
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
1349
|
+
json(res, 405, { detail: "method not allowed" });
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
await serveStatic(res, path);
|
|
1353
|
+
};
|
|
1354
|
+
const server = http.createServer((req, res) => {
|
|
1355
|
+
void handler(req, res).catch((e) => {
|
|
1356
|
+
deps.log(`[unit-web] \u8BF7\u6C42\u5904\u7406\u5931\u8D25: ${e?.message || e}`);
|
|
1357
|
+
if (!res.headersSent) json(res, 500, { detail: "internal error" });
|
|
1358
|
+
});
|
|
1359
|
+
});
|
|
1360
|
+
server.requestTimeout = 60 * 60 * 1e3;
|
|
1361
|
+
return new Promise((resolve8, reject) => {
|
|
1362
|
+
if (deps.routeUpgrade) server.on("upgrade", (req, socket, head) => {
|
|
1363
|
+
if (!deps.routeUpgrade(req, socket, head)) socket.destroy();
|
|
1364
|
+
});
|
|
1365
|
+
const sockets = /* @__PURE__ */ new Set();
|
|
1366
|
+
server.on("connection", (socket) => {
|
|
1367
|
+
sockets.add(socket);
|
|
1368
|
+
socket.once("close", () => sockets.delete(socket));
|
|
1369
|
+
});
|
|
1370
|
+
server.once("error", reject);
|
|
1371
|
+
server.listen(opts.port, opts.bindHost ?? "0.0.0.0", () => {
|
|
1372
|
+
const { port } = server.address();
|
|
1373
|
+
resolve8({
|
|
1374
|
+
port,
|
|
1375
|
+
internalSecret,
|
|
1376
|
+
p2pSecret,
|
|
1377
|
+
proxyCallerKey,
|
|
1378
|
+
close: () => new Promise((r) => {
|
|
1379
|
+
server.close(() => r());
|
|
1380
|
+
for (const socket of sockets) socket.destroy();
|
|
1381
|
+
})
|
|
1382
|
+
});
|
|
1383
|
+
});
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// desktop/products/basic.json
|
|
1388
|
+
var basic_default = {
|
|
1389
|
+
id: "basic",
|
|
1390
|
+
displayName: "Forsion Basic",
|
|
1391
|
+
appId: "com.forsion.basic",
|
|
1392
|
+
productName: "Forsion Basic",
|
|
1393
|
+
artifactPrefix: "ForsionBasic",
|
|
1394
|
+
defaultSpace: "home",
|
|
1395
|
+
spaces: ["home"],
|
|
1396
|
+
agentBackend: false,
|
|
1397
|
+
market: true,
|
|
1398
|
+
unit: true
|
|
1399
|
+
};
|
|
1400
|
+
|
|
1401
|
+
// unit/packages.ts
|
|
1402
|
+
import { readFile as readFile2, readdir, realpath as realpath2, stat } from "node:fs/promises";
|
|
1403
|
+
import { isAbsolute, resolve, sep as sep2 } from "node:path";
|
|
1404
|
+
|
|
1405
|
+
// desktop/shared/amadeus/pluginFiles.ts
|
|
1406
|
+
function isSafePluginExt(extension) {
|
|
1407
|
+
if (typeof extension !== "string") return false;
|
|
1408
|
+
const value = extension.trim().toLowerCase();
|
|
1409
|
+
if (!value.startsWith(".") || value.length < 2 || value.length > 40) return false;
|
|
1410
|
+
if (value === ".md" || value === ".markdown" || value === ".txt") return false;
|
|
1411
|
+
return value.endsWith(".md") ? /^\.[a-z0-9][a-z0-9-]*\.md$/.test(value) : /^\.[a-z0-9][a-z0-9.-]*$/.test(value);
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// desktop/shared/nativeFeatures.ts
|
|
1415
|
+
var NATIVE_FEATURE_IDS = ["amadeus", "tangu", "calendar", "automation", "public"];
|
|
1416
|
+
var NATIVE_FEATURE_SPACES = {
|
|
1417
|
+
amadeus: ["amadeus"],
|
|
1418
|
+
tangu: ["tangu"],
|
|
1419
|
+
calendar: ["calendar"],
|
|
1420
|
+
automation: ["automation"],
|
|
1421
|
+
public: ["public"]
|
|
1422
|
+
};
|
|
1423
|
+
var isNativeFeatureId = (value) => typeof value === "string" && NATIVE_FEATURE_IDS.includes(value);
|
|
1424
|
+
function nativeFeatureSpaceIds(features) {
|
|
1425
|
+
return [...new Set(features.flatMap((feature) => NATIVE_FEATURE_SPACES[feature]))];
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// unit/packages.ts
|
|
1429
|
+
var validId = (id) => typeof id === "string" && /^[a-z][a-z0-9-]*$/.test(id);
|
|
1430
|
+
function versionAtLeast(actual, minimum) {
|
|
1431
|
+
const parse = (v) => /^\d+\.\d+\.\d+(?:[-+][\w.-]+)?$/.test(v) ? v.split(/[.+-]/).slice(0, 3).map(Number) : null;
|
|
1432
|
+
const a = parse(actual), b = parse(minimum);
|
|
1433
|
+
if (!a || !b) throw new Error("Invalid semantic version");
|
|
1434
|
+
for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] > b[i];
|
|
1435
|
+
return true;
|
|
1436
|
+
}
|
|
1437
|
+
async function packagePath(root, relative2) {
|
|
1438
|
+
if (typeof relative2 !== "string" || !relative2 || isAbsolute(relative2) || relative2.split(/[\\/]/).includes("..")) throw new Error("Plugin file escapes its package");
|
|
1439
|
+
const file2 = await realpath2(resolve(root, relative2));
|
|
1440
|
+
if (!file2.startsWith(root + sep2)) throw new Error("Plugin file escapes its package");
|
|
1441
|
+
if (!(await stat(file2)).isFile()) throw new Error("Plugin entry must be a regular file");
|
|
1442
|
+
return file2;
|
|
1443
|
+
}
|
|
1444
|
+
async function readPackage(dir, appVersion) {
|
|
1445
|
+
const root = await realpath2(dir);
|
|
1446
|
+
const manifest = JSON.parse(await readFile2(await packagePath(root, "manifest.json"), "utf8"));
|
|
1447
|
+
if (!validId(manifest.id) || manifest.apiVersion !== 1) throw new Error("Invalid plugin identity or unsupported plugin API");
|
|
1448
|
+
versionAtLeast(manifest.version, "0.0.0");
|
|
1449
|
+
if (manifest.minAppVersion && appVersion && !versionAtLeast(appVersion, manifest.minAppVersion)) throw new Error(`Plugin ${manifest.id} requires Unit ${manifest.minAppVersion}`);
|
|
1450
|
+
if (manifest.requires && (!Array.isArray(manifest.requires) || !manifest.requires.every(validId))) throw new Error("Invalid plugin dependencies");
|
|
1451
|
+
const result = { root, manifest, spaces: [], spaceIds: /* @__PURE__ */ new Set() };
|
|
1452
|
+
if (manifest.frontend) {
|
|
1453
|
+
const { features, services = {} } = manifest.frontend;
|
|
1454
|
+
if (!Array.isArray(features) || !features.length || !features.every(isNativeFeatureId) || new Set(features).size !== features.length) throw new Error("Invalid native frontend features");
|
|
1455
|
+
for (const [id, service] of Object.entries(services)) {
|
|
1456
|
+
if (!["amadeus", "tangu"].includes(id) || !features.includes(id) || !service || service.adapter !== "forsion-cloud-v1" || typeof service.apiBase !== "string" || !/^\/(?:[A-Za-z0-9_-]+\/?)+$/.test(service.apiBase) || id === "tangu" && service.execution !== "fleet" || id === "amadeus" && service.collaboration !== void 0 && typeof service.collaboration !== "boolean") {
|
|
1457
|
+
throw new Error("Invalid frontend cloud service");
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
for (const id of nativeFeatureSpaceIds(features)) result.spaceIds.add(id);
|
|
1461
|
+
}
|
|
1462
|
+
if (manifest.main) {
|
|
1463
|
+
const code = await readFile2(await packagePath(root, manifest.main), "utf8");
|
|
1464
|
+
result.ui = {
|
|
1465
|
+
id: manifest.id,
|
|
1466
|
+
name: manifest.name,
|
|
1467
|
+
nameEn: manifest.nameEn,
|
|
1468
|
+
version: manifest.version,
|
|
1469
|
+
apiVersion: manifest.apiVersion,
|
|
1470
|
+
minAppVersion: manifest.minAppVersion,
|
|
1471
|
+
description: manifest.description,
|
|
1472
|
+
descriptionEn: manifest.descriptionEn,
|
|
1473
|
+
onboarding: manifest.onboarding,
|
|
1474
|
+
fileExtensions: Array.isArray(manifest.fileExtensions) ? manifest.fileExtensions.filter(isSafePluginExt).map((v) => v.trim().toLowerCase()).slice(0, 8) : [],
|
|
1475
|
+
code
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
if (manifest.runtime) {
|
|
1479
|
+
if (manifest.runtime.apiVersion !== 1) throw new Error("Unsupported local runtime API");
|
|
1480
|
+
if (manifest.runtime.dependencies?.mode === "npm-ci") throw new Error("Local runtime dependencies must be bundled");
|
|
1481
|
+
result.runtimeEntry = await packagePath(root, manifest.runtime.main);
|
|
1482
|
+
}
|
|
1483
|
+
if (manifest.backend) {
|
|
1484
|
+
if (manifest.backend.apiVersion !== 1) throw new Error("Unsupported backend plugin API");
|
|
1485
|
+
result.backendEntry = await packagePath(root, manifest.backend.main);
|
|
1486
|
+
}
|
|
1487
|
+
for (const deps of [manifest.backend?.dependencies, manifest.runtime?.dependencies]) {
|
|
1488
|
+
if (deps && !["bundled", "npm-ci"].includes(deps.mode)) throw new Error("Unsupported dependency mode");
|
|
1489
|
+
const libc = process.platform === "linux" ? process.report?.getReport()?.header?.glibcVersionRuntime ? "glibc" : "musl" : void 0;
|
|
1490
|
+
if (deps?.mode === "bundled" && (deps.platform && deps.platform !== process.platform || deps.libc && deps.libc !== libc || deps.arch && deps.arch !== process.arch || deps.nodeAbi && deps.nodeAbi !== process.versions.modules)) {
|
|
1491
|
+
throw new Error("Plugin dependencies were built for a different OS, architecture, Node ABI, or libc");
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
if (!result.ui && !result.backendEntry && !result.runtimeEntry && !manifest.frontend) throw new Error("Plugin has no UI or backend entry");
|
|
1495
|
+
const entries = await readdir(resolve(root, "spaces"), { withFileTypes: true }).catch((error) => {
|
|
1496
|
+
if (error.code === "ENOENT") return [];
|
|
1497
|
+
throw error;
|
|
1498
|
+
});
|
|
1499
|
+
for (const entry of entries) {
|
|
1500
|
+
if (!entry.isDirectory()) continue;
|
|
1501
|
+
const json = await readFile2(await packagePath(root, `spaces/${entry.name}/space.json`), "utf8");
|
|
1502
|
+
const spec = JSON.parse(json);
|
|
1503
|
+
if (!validId(spec.id) || result.spaceIds.has(spec.id)) throw new Error("Invalid or duplicate Space id");
|
|
1504
|
+
result.spaceIds.add(spec.id);
|
|
1505
|
+
result.spaces.push({ slug: entry.name, json, plugin: manifest.id });
|
|
1506
|
+
}
|
|
1507
|
+
return result;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// unit/backendRunner.ts
|
|
1511
|
+
import http2 from "node:http";
|
|
1512
|
+
import { isAbsolute as isAbsolute2 } from "node:path";
|
|
1513
|
+
import { MessageChannel, Worker } from "node:worker_threads";
|
|
1514
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1515
|
+
|
|
1516
|
+
// unit/portDuplex.ts
|
|
1517
|
+
import { Duplex } from "node:stream";
|
|
1518
|
+
var PortDuplex = class extends Duplex {
|
|
1519
|
+
constructor(port, metadata = {}, options = {}) {
|
|
1520
|
+
super({ allowHalfOpen: true, ...options });
|
|
1521
|
+
this.port = port;
|
|
1522
|
+
Object.assign(this, metadata);
|
|
1523
|
+
this.on("error", () => {
|
|
1524
|
+
});
|
|
1525
|
+
port.on("message", (packet) => {
|
|
1526
|
+
if (this.destroyed) return;
|
|
1527
|
+
this.touch();
|
|
1528
|
+
if (packet.type === "data") {
|
|
1529
|
+
if (this.push(Buffer.from(packet.bytes))) this.send({ type: "ack" });
|
|
1530
|
+
else this.awaitingRead = true;
|
|
1531
|
+
} else if (packet.type === "ack") {
|
|
1532
|
+
const callback = this.pendingWrite;
|
|
1533
|
+
this.pendingWrite = void 0;
|
|
1534
|
+
callback?.();
|
|
1535
|
+
} else if (packet.type === "end") {
|
|
1536
|
+
this.receivedEnd = true;
|
|
1537
|
+
this.push(null);
|
|
1538
|
+
} else if (packet.type === "close") {
|
|
1539
|
+
this.destroy();
|
|
1540
|
+
}
|
|
1541
|
+
});
|
|
1542
|
+
port.on("messageerror", () => this.destroy(new Error("Invalid backend connection message")));
|
|
1543
|
+
port.on("close", () => {
|
|
1544
|
+
if (!this.destroyed) this.destroy(this.receivedEnd ? void 0 : new Error("Backend connection closed"));
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
connecting = false;
|
|
1548
|
+
pending = false;
|
|
1549
|
+
remoteAddress;
|
|
1550
|
+
remotePort;
|
|
1551
|
+
remoteFamily;
|
|
1552
|
+
localAddress;
|
|
1553
|
+
localPort;
|
|
1554
|
+
encrypted;
|
|
1555
|
+
pendingWrite;
|
|
1556
|
+
awaitingRead = false;
|
|
1557
|
+
receivedEnd = false;
|
|
1558
|
+
timeoutMs = 0;
|
|
1559
|
+
timeoutTimer;
|
|
1560
|
+
send(packet) {
|
|
1561
|
+
try {
|
|
1562
|
+
this.port.postMessage(packet);
|
|
1563
|
+
} catch {
|
|
1564
|
+
this.destroy(new Error("Backend connection unavailable"));
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
touch() {
|
|
1568
|
+
if (!this.timeoutMs) return;
|
|
1569
|
+
if (this.timeoutTimer) clearTimeout(this.timeoutTimer);
|
|
1570
|
+
this.timeoutTimer = setTimeout(() => this.emit("timeout"), this.timeoutMs);
|
|
1571
|
+
this.timeoutTimer.unref();
|
|
1572
|
+
}
|
|
1573
|
+
_read() {
|
|
1574
|
+
if (this.awaitingRead) {
|
|
1575
|
+
this.awaitingRead = false;
|
|
1576
|
+
this.send({ type: "ack" });
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
_write(chunk, encoding, callback) {
|
|
1580
|
+
this.touch();
|
|
1581
|
+
this.pendingWrite = callback;
|
|
1582
|
+
this.send({ type: "data", bytes: typeof chunk === "string" ? Buffer.from(chunk, encoding) : chunk });
|
|
1583
|
+
}
|
|
1584
|
+
_final(callback) {
|
|
1585
|
+
this.send({ type: "end" });
|
|
1586
|
+
callback();
|
|
1587
|
+
}
|
|
1588
|
+
_destroy(error, callback) {
|
|
1589
|
+
if (this.timeoutTimer) clearTimeout(this.timeoutTimer);
|
|
1590
|
+
const pending = this.pendingWrite;
|
|
1591
|
+
this.pendingWrite = void 0;
|
|
1592
|
+
pending?.(error ?? new Error("Backend connection closed"));
|
|
1593
|
+
try {
|
|
1594
|
+
this.port.postMessage({ type: "close" });
|
|
1595
|
+
} catch {
|
|
1596
|
+
}
|
|
1597
|
+
this.port.close();
|
|
1598
|
+
callback(error);
|
|
1599
|
+
}
|
|
1600
|
+
setTimeout(milliseconds, callback) {
|
|
1601
|
+
if (callback) this.once("timeout", callback);
|
|
1602
|
+
this.timeoutMs = milliseconds;
|
|
1603
|
+
if (this.timeoutTimer) clearTimeout(this.timeoutTimer);
|
|
1604
|
+
this.timeoutTimer = void 0;
|
|
1605
|
+
this.touch();
|
|
1606
|
+
return this;
|
|
1607
|
+
}
|
|
1608
|
+
setNoDelay() {
|
|
1609
|
+
return this;
|
|
1610
|
+
}
|
|
1611
|
+
setKeepAlive() {
|
|
1612
|
+
return this;
|
|
1613
|
+
}
|
|
1614
|
+
ref() {
|
|
1615
|
+
this.port.ref();
|
|
1616
|
+
return this;
|
|
1617
|
+
}
|
|
1618
|
+
unref() {
|
|
1619
|
+
this.port.unref();
|
|
1620
|
+
this.timeoutTimer?.unref();
|
|
1621
|
+
return this;
|
|
1622
|
+
}
|
|
1623
|
+
address() {
|
|
1624
|
+
return { address: this.localAddress ?? "", family: this.remoteFamily ?? "IPv4", port: this.localPort ?? 0 };
|
|
1625
|
+
}
|
|
1626
|
+
};
|
|
1627
|
+
|
|
1628
|
+
// unit/backendRunner.ts
|
|
1629
|
+
function environment(overrides) {
|
|
1630
|
+
const env = { ...process.env };
|
|
1631
|
+
for (const [key, value] of Object.entries(overrides ?? {})) {
|
|
1632
|
+
if (value === void 0) delete env[key];
|
|
1633
|
+
else env[key] = value;
|
|
1634
|
+
}
|
|
1635
|
+
return env;
|
|
1636
|
+
}
|
|
1637
|
+
function socketMetadata(socket) {
|
|
1638
|
+
return {
|
|
1639
|
+
remoteAddress: socket.remoteAddress,
|
|
1640
|
+
remotePort: socket.remotePort,
|
|
1641
|
+
remoteFamily: socket.remoteFamily,
|
|
1642
|
+
localAddress: socket.localAddress,
|
|
1643
|
+
localPort: socket.localPort,
|
|
1644
|
+
encrypted: Boolean(socket.encrypted)
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
function unavailable(response) {
|
|
1648
|
+
if (response.destroyed || response.writableEnded) return;
|
|
1649
|
+
if (response.headersSent) {
|
|
1650
|
+
response.destroy();
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
response.writeHead(503, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
1654
|
+
response.end(JSON.stringify({ detail: "Backend plugin unavailable", code: "BACKEND_UNAVAILABLE" }));
|
|
1655
|
+
}
|
|
1656
|
+
var BackendSession = class {
|
|
1657
|
+
constructor(options, mode) {
|
|
1658
|
+
this.options = options;
|
|
1659
|
+
this.mode = mode;
|
|
1660
|
+
for (const path of [options.entry, options.packageDir, options.dataDir, options.workerFile]) {
|
|
1661
|
+
if (!isAbsolute2(path)) throw new Error("Backend runtime paths must be absolute");
|
|
1662
|
+
}
|
|
1663
|
+
if (options.signal?.aborted) throw this.abortReason();
|
|
1664
|
+
if (options.accountTimeoutMs !== void 0 && (!Number.isFinite(options.accountTimeoutMs) || options.accountTimeoutMs <= 0)) {
|
|
1665
|
+
throw new Error("Backend account timeout must be positive and finite");
|
|
1666
|
+
}
|
|
1667
|
+
this.state = mode === "start" ? "starting" : "migrating";
|
|
1668
|
+
this.completion = new Promise((resolve8, reject) => {
|
|
1669
|
+
this.resolveCompletion = resolve8;
|
|
1670
|
+
this.rejectCompletion = reject;
|
|
1671
|
+
});
|
|
1672
|
+
const data = {
|
|
1673
|
+
id: options.id,
|
|
1674
|
+
entry: options.entry,
|
|
1675
|
+
packageDir: options.packageDir,
|
|
1676
|
+
dataDir: options.dataDir,
|
|
1677
|
+
config: options.config ?? {},
|
|
1678
|
+
mode
|
|
1679
|
+
};
|
|
1680
|
+
this.worker = new Worker(options.workerFile, { workerData: data, env: environment(options.env), execArgv: [] });
|
|
1681
|
+
this.worker.on("message", (event) => this.onMessage(event));
|
|
1682
|
+
this.worker.on("error", (error) => this.fail(error));
|
|
1683
|
+
this.worker.on("exit", (code) => {
|
|
1684
|
+
if (this.cancellationError) this.finishCancellation();
|
|
1685
|
+
else if (!this.expectedExit) this.fail(new Error(`Backend worker exited unexpectedly (${code})`));
|
|
1686
|
+
});
|
|
1687
|
+
this.startTimer = setTimeout(() => this.cancel(new Error(`Backend ${mode} timed out`)), options.startTimeoutMs ?? 3e4);
|
|
1688
|
+
this.startTimer.unref();
|
|
1689
|
+
if (options.signal) {
|
|
1690
|
+
this.abortHandler = () => this.cancel(this.abortReason());
|
|
1691
|
+
options.signal.addEventListener("abort", this.abortHandler, { once: true });
|
|
1692
|
+
if (options.signal.aborted) this.abortHandler();
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
state;
|
|
1696
|
+
mounts = [];
|
|
1697
|
+
hasAccount = false;
|
|
1698
|
+
accountMetadata;
|
|
1699
|
+
worker;
|
|
1700
|
+
completion;
|
|
1701
|
+
resolveCompletion;
|
|
1702
|
+
rejectCompletion;
|
|
1703
|
+
completed = false;
|
|
1704
|
+
failureReported = false;
|
|
1705
|
+
expectedExit = false;
|
|
1706
|
+
startTimer;
|
|
1707
|
+
stopPromise;
|
|
1708
|
+
resolveStop;
|
|
1709
|
+
rejectStop;
|
|
1710
|
+
stopTimer;
|
|
1711
|
+
abortHandler;
|
|
1712
|
+
cancellationError;
|
|
1713
|
+
cancellationFinish;
|
|
1714
|
+
active = /* @__PURE__ */ new Set();
|
|
1715
|
+
accountCalls = /* @__PURE__ */ new Map();
|
|
1716
|
+
abortReason() {
|
|
1717
|
+
const reason = this.options.signal?.reason;
|
|
1718
|
+
return reason instanceof Error ? reason : new Error("Backend operation cancelled", { cause: reason });
|
|
1719
|
+
}
|
|
1720
|
+
detachAbort() {
|
|
1721
|
+
if (this.abortHandler) this.options.signal?.removeEventListener("abort", this.abortHandler);
|
|
1722
|
+
this.abortHandler = void 0;
|
|
1723
|
+
}
|
|
1724
|
+
/** A startup deadline must abort plugin-owned jobs before their worker disappears. */
|
|
1725
|
+
cancel(error) {
|
|
1726
|
+
if (this.completed || this.cancellationError || this.state === "stopped" || this.state === "failed") return;
|
|
1727
|
+
this.cancellationError = error;
|
|
1728
|
+
this.state = "stopping";
|
|
1729
|
+
if (this.startTimer) clearTimeout(this.startTimer);
|
|
1730
|
+
this.detachAbort();
|
|
1731
|
+
this.closeConnections();
|
|
1732
|
+
this.stopTimer = setTimeout(() => this.finishCancellation(), this.options.stopTimeoutMs ?? 1e4);
|
|
1733
|
+
this.stopTimer.unref();
|
|
1734
|
+
try {
|
|
1735
|
+
this.worker.postMessage({ type: "stop" });
|
|
1736
|
+
} catch {
|
|
1737
|
+
this.finishCancellation();
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
finishCancellation() {
|
|
1741
|
+
if (!this.cancellationError || this.cancellationFinish) return;
|
|
1742
|
+
this.expectedExit = true;
|
|
1743
|
+
if (this.stopTimer) clearTimeout(this.stopTimer);
|
|
1744
|
+
this.cancellationFinish = this.worker.terminate().then(() => {
|
|
1745
|
+
}, () => {
|
|
1746
|
+
}).then(() => {
|
|
1747
|
+
this.settleFailure(this.cancellationError);
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
callback(callback) {
|
|
1751
|
+
try {
|
|
1752
|
+
Promise.resolve(callback?.()).catch((error) => this.fail(error));
|
|
1753
|
+
} catch (error) {
|
|
1754
|
+
this.fail(error);
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
onMessage(raw) {
|
|
1758
|
+
if (!raw || typeof raw !== "object" || !("type" in raw)) {
|
|
1759
|
+
this.fail(new Error("Invalid backend worker event"));
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1762
|
+
const event = raw;
|
|
1763
|
+
if (this.cancellationError) {
|
|
1764
|
+
if (event.type === "stopped" || event.type === "failure" || event.type === "migrated") this.finishCancellation();
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
if (event.type === "failure") {
|
|
1768
|
+
const error = new Error(typeof event.message === "string" ? event.message : "Backend worker failed");
|
|
1769
|
+
if (typeof event.stack === "string") error.stack = event.stack;
|
|
1770
|
+
this.fail(error);
|
|
1771
|
+
} else if (event.type === "ready" && this.state === "starting") {
|
|
1772
|
+
if (!Array.isArray(event.mounts) || event.mounts.some((mount) => typeof mount !== "string")) {
|
|
1773
|
+
this.fail(new Error("Invalid backend mount registration"));
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
this.mounts = Object.freeze([...event.mounts]);
|
|
1777
|
+
this.hasAccount = event.hasAccount === true;
|
|
1778
|
+
this.accountMetadata = event.accountMetadata && Object.freeze({ ...event.accountMetadata });
|
|
1779
|
+
this.state = "running";
|
|
1780
|
+
this.finish();
|
|
1781
|
+
} else if (event.type === "migrated" && this.state === "migrating") {
|
|
1782
|
+
this.expectedExit = true;
|
|
1783
|
+
void this.worker.terminate().then(() => {
|
|
1784
|
+
if (this.cancellationError) {
|
|
1785
|
+
this.finishCancellation();
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
this.state = "stopped";
|
|
1789
|
+
this.finish();
|
|
1790
|
+
}, (error) => this.fail(error));
|
|
1791
|
+
} else if (event.type === "stopped" && this.state === "stopping") {
|
|
1792
|
+
this.expectedExit = true;
|
|
1793
|
+
void this.worker.terminate().then(() => {
|
|
1794
|
+
if (this.stopTimer) clearTimeout(this.stopTimer);
|
|
1795
|
+
this.closeConnections();
|
|
1796
|
+
this.state = "stopped";
|
|
1797
|
+
this.resolveStop?.();
|
|
1798
|
+
}, (error) => this.fail(error));
|
|
1799
|
+
} else if (event.type === "account-result") {
|
|
1800
|
+
this.accountCalls.get(event.requestId)?.finish(event.identity);
|
|
1801
|
+
} else if (event.type === "account-error") {
|
|
1802
|
+
this.accountCalls.get(event.requestId)?.finish(null, new Error("Account provider unavailable"));
|
|
1803
|
+
} else if (event.type === "restart" && this.state === "running") {
|
|
1804
|
+
this.callback(this.options.onRestart);
|
|
1805
|
+
} else if (event.type === "log" && typeof event.message === "string") {
|
|
1806
|
+
try {
|
|
1807
|
+
this.options.onLog?.(event.message);
|
|
1808
|
+
} catch {
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
finish() {
|
|
1813
|
+
if (this.startTimer) clearTimeout(this.startTimer);
|
|
1814
|
+
this.detachAbort();
|
|
1815
|
+
if (this.completed) return;
|
|
1816
|
+
this.completed = true;
|
|
1817
|
+
this.resolveCompletion();
|
|
1818
|
+
}
|
|
1819
|
+
fail(cause) {
|
|
1820
|
+
if (this.state === "stopped") return;
|
|
1821
|
+
if (this.cancellationError) {
|
|
1822
|
+
this.finishCancellation();
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
1826
|
+
this.expectedExit = true;
|
|
1827
|
+
this.settleFailure(error);
|
|
1828
|
+
void this.worker.terminate().catch(() => {
|
|
1829
|
+
});
|
|
1830
|
+
}
|
|
1831
|
+
settleFailure(error) {
|
|
1832
|
+
this.state = "failed";
|
|
1833
|
+
this.detachAbort();
|
|
1834
|
+
if (this.startTimer) clearTimeout(this.startTimer);
|
|
1835
|
+
if (this.stopTimer) clearTimeout(this.stopTimer);
|
|
1836
|
+
this.closeConnections();
|
|
1837
|
+
if (!this.completed) {
|
|
1838
|
+
this.completed = true;
|
|
1839
|
+
this.rejectCompletion(error);
|
|
1840
|
+
}
|
|
1841
|
+
this.rejectStop?.(error);
|
|
1842
|
+
if (!this.failureReported) {
|
|
1843
|
+
this.failureReported = true;
|
|
1844
|
+
try {
|
|
1845
|
+
this.options.onFailure?.(error);
|
|
1846
|
+
} catch {
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
closeConnections() {
|
|
1851
|
+
for (const socket of this.active) socket.destroy(new Error("Backend plugin unavailable"));
|
|
1852
|
+
this.active.clear();
|
|
1853
|
+
for (const call of this.accountCalls.values()) call.finish(null, new Error("Account provider unavailable"));
|
|
1854
|
+
}
|
|
1855
|
+
resolveAccount = async (token, options = {}) => {
|
|
1856
|
+
if (this.state !== "running" || !this.hasAccount) throw new Error("Account provider unavailable");
|
|
1857
|
+
if (typeof token !== "string" || !token || token.length > 16384) return null;
|
|
1858
|
+
if (options.signal?.aborted) throw new Error("Account resolution cancelled");
|
|
1859
|
+
if (this.accountCalls.size >= 256) throw new Error("Account provider busy");
|
|
1860
|
+
const requestId = randomUUID2();
|
|
1861
|
+
return new Promise((resolve8, reject) => {
|
|
1862
|
+
let timer;
|
|
1863
|
+
const finish = (identity, error) => {
|
|
1864
|
+
if (!this.accountCalls.delete(requestId)) return;
|
|
1865
|
+
clearTimeout(timer);
|
|
1866
|
+
options.signal?.removeEventListener("abort", cancel);
|
|
1867
|
+
if (error) reject(error);
|
|
1868
|
+
else resolve8(identity);
|
|
1869
|
+
};
|
|
1870
|
+
const abort = (message) => {
|
|
1871
|
+
finish(null, new Error(message));
|
|
1872
|
+
try {
|
|
1873
|
+
this.worker.postMessage({ type: "account-cancel", requestId });
|
|
1874
|
+
} catch {
|
|
1875
|
+
}
|
|
1876
|
+
};
|
|
1877
|
+
const cancel = () => abort("Account resolution cancelled");
|
|
1878
|
+
this.accountCalls.set(requestId, { finish });
|
|
1879
|
+
timer = setTimeout(() => abort("Account resolution timed out"), Math.min(this.options.accountTimeoutMs ?? 5e3, 3e4));
|
|
1880
|
+
timer.unref();
|
|
1881
|
+
options.signal?.addEventListener("abort", cancel, { once: true });
|
|
1882
|
+
if (options.signal?.aborted) {
|
|
1883
|
+
cancel();
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
try {
|
|
1887
|
+
this.worker.postMessage({ type: "account-resolve", requestId, token });
|
|
1888
|
+
} catch {
|
|
1889
|
+
finish(null, new Error("Account provider unavailable"));
|
|
1890
|
+
}
|
|
1891
|
+
});
|
|
1892
|
+
};
|
|
1893
|
+
connection(metadata) {
|
|
1894
|
+
const { port1, port2 } = new MessageChannel();
|
|
1895
|
+
const stream = new PortDuplex(port1);
|
|
1896
|
+
this.active.add(stream);
|
|
1897
|
+
stream.once("close", () => this.active.delete(stream));
|
|
1898
|
+
try {
|
|
1899
|
+
this.worker.postMessage({ type: "connection", port: port2, socket: metadata }, [port2]);
|
|
1900
|
+
} catch (error) {
|
|
1901
|
+
port2.close();
|
|
1902
|
+
stream.destroy();
|
|
1903
|
+
throw error;
|
|
1904
|
+
}
|
|
1905
|
+
return stream;
|
|
1906
|
+
}
|
|
1907
|
+
handle = (request, response) => {
|
|
1908
|
+
if (this.state !== "running") {
|
|
1909
|
+
unavailable(response);
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
let connection;
|
|
1913
|
+
try {
|
|
1914
|
+
connection = this.connection(socketMetadata(request.socket));
|
|
1915
|
+
} catch {
|
|
1916
|
+
unavailable(response);
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
const agent = new http2.Agent({ keepAlive: false });
|
|
1920
|
+
agent.createConnection = () => connection;
|
|
1921
|
+
const upstream = http2.request({
|
|
1922
|
+
hostname: "unit-backend.invalid",
|
|
1923
|
+
port: 80,
|
|
1924
|
+
path: request.url,
|
|
1925
|
+
method: request.method,
|
|
1926
|
+
headers: { ...request.headers, connection: "close" },
|
|
1927
|
+
agent
|
|
1928
|
+
});
|
|
1929
|
+
const cancel = () => {
|
|
1930
|
+
upstream.destroy();
|
|
1931
|
+
connection.destroy();
|
|
1932
|
+
agent.destroy();
|
|
1933
|
+
};
|
|
1934
|
+
request.once("aborted", cancel);
|
|
1935
|
+
request.once("error", cancel);
|
|
1936
|
+
request.once("end", () => {
|
|
1937
|
+
if (Object.keys(request.trailers).length) upstream.addTrailers(request.trailers);
|
|
1938
|
+
});
|
|
1939
|
+
response.once("close", cancel);
|
|
1940
|
+
upstream.once("error", () => {
|
|
1941
|
+
unavailable(response);
|
|
1942
|
+
agent.destroy();
|
|
1943
|
+
});
|
|
1944
|
+
upstream.once("response", (incoming) => {
|
|
1945
|
+
if (response.destroyed) {
|
|
1946
|
+
incoming.destroy();
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
const headers = { ...incoming.headers };
|
|
1950
|
+
delete headers.connection;
|
|
1951
|
+
delete headers["keep-alive"];
|
|
1952
|
+
response.writeHead(incoming.statusCode ?? 502, incoming.statusMessage, headers);
|
|
1953
|
+
incoming.once("aborted", () => response.destroy());
|
|
1954
|
+
incoming.once("error", () => response.destroy());
|
|
1955
|
+
incoming.once("end", () => {
|
|
1956
|
+
if (Object.keys(incoming.trailers).length) response.addTrailers(incoming.trailers);
|
|
1957
|
+
});
|
|
1958
|
+
incoming.pipe(response);
|
|
1959
|
+
});
|
|
1960
|
+
request.pipe(upstream);
|
|
1961
|
+
};
|
|
1962
|
+
upgrade = (request, socket, head) => {
|
|
1963
|
+
if (this.state !== "running") {
|
|
1964
|
+
socket.end("HTTP/1.1 503 Service Unavailable\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
let connection;
|
|
1968
|
+
try {
|
|
1969
|
+
connection = this.connection(socketMetadata(request.socket));
|
|
1970
|
+
} catch {
|
|
1971
|
+
socket.destroy();
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1974
|
+
const close = () => {
|
|
1975
|
+
connection.destroy();
|
|
1976
|
+
socket.destroy();
|
|
1977
|
+
};
|
|
1978
|
+
socket.once("error", close);
|
|
1979
|
+
socket.once("close", () => connection.destroy());
|
|
1980
|
+
connection.once("error", close);
|
|
1981
|
+
connection.once("close", () => socket.destroy());
|
|
1982
|
+
const lines = [`${request.method ?? "GET"} ${request.url ?? "/"} HTTP/${request.httpVersion}`];
|
|
1983
|
+
for (let i = 0; i < request.rawHeaders.length; i += 2) lines.push(`${request.rawHeaders[i]}: ${request.rawHeaders[i + 1]}`);
|
|
1984
|
+
connection.write(lines.join("\r\n") + "\r\n\r\n");
|
|
1985
|
+
if (head.length) connection.write(head);
|
|
1986
|
+
socket.pipe(connection).pipe(socket);
|
|
1987
|
+
};
|
|
1988
|
+
stop = () => {
|
|
1989
|
+
if (this.stopPromise) return this.stopPromise;
|
|
1990
|
+
if (this.state === "stopped") return Promise.resolve();
|
|
1991
|
+
if (this.state === "failed") return this.worker.terminate().then(() => {
|
|
1992
|
+
});
|
|
1993
|
+
this.state = "stopping";
|
|
1994
|
+
this.closeConnections();
|
|
1995
|
+
this.stopPromise = new Promise((resolve8, reject) => {
|
|
1996
|
+
this.resolveStop = resolve8;
|
|
1997
|
+
this.rejectStop = reject;
|
|
1998
|
+
});
|
|
1999
|
+
this.stopTimer = setTimeout(() => this.fail(new Error("Backend stop timed out")), this.options.stopTimeoutMs ?? 1e4);
|
|
2000
|
+
this.stopTimer.unref();
|
|
2001
|
+
this.worker.postMessage({ type: "stop" });
|
|
2002
|
+
return this.stopPromise;
|
|
2003
|
+
};
|
|
2004
|
+
};
|
|
2005
|
+
async function startBackend(options) {
|
|
2006
|
+
const session = new BackendSession(options, "start");
|
|
2007
|
+
await session.completion;
|
|
2008
|
+
return {
|
|
2009
|
+
get mounts() {
|
|
2010
|
+
return session.mounts;
|
|
2011
|
+
},
|
|
2012
|
+
get state() {
|
|
2013
|
+
return session.state;
|
|
2014
|
+
},
|
|
2015
|
+
account: session.hasAccount ? { metadata: session.accountMetadata, resolve: session.resolveAccount } : void 0,
|
|
2016
|
+
handle: session.handle,
|
|
2017
|
+
upgrade: session.upgrade,
|
|
2018
|
+
stop: session.stop
|
|
2019
|
+
};
|
|
2020
|
+
}
|
|
2021
|
+
async function migrateBackend(options) {
|
|
2022
|
+
await new BackendSession(options, "migrate").completion;
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
// unit/accountHttp.ts
|
|
2026
|
+
import { createHash as createHash2, randomUUID as randomUUID3 } from "node:crypto";
|
|
2027
|
+
import { mkdir, readFile as readFile3, rename, writeFile, rm } from "node:fs/promises";
|
|
2028
|
+
import { join } from "node:path";
|
|
2029
|
+
|
|
2030
|
+
// desktop/shared/unitPreferences.ts
|
|
2031
|
+
var UNIT_PREFERENCE_KEYS = [
|
|
2032
|
+
"modelId",
|
|
2033
|
+
"asrModelId",
|
|
2034
|
+
"visionModelId",
|
|
2035
|
+
"visionMode",
|
|
2036
|
+
"backgroundModelId",
|
|
2037
|
+
"agentDeskEnabled",
|
|
2038
|
+
"summaryOpenIn",
|
|
2039
|
+
"ttsModelId",
|
|
2040
|
+
"ttsVoice",
|
|
2041
|
+
"ttsSpeed",
|
|
2042
|
+
"ttsAutoSpeak",
|
|
2043
|
+
"asrBackend",
|
|
2044
|
+
"lastApprovalMode",
|
|
2045
|
+
"lastThinkingLevel",
|
|
2046
|
+
"lastChatThinkingLevel",
|
|
2047
|
+
"notesAttachmentMode",
|
|
2048
|
+
"notesAttachmentFolder",
|
|
2049
|
+
"notesImportPreview",
|
|
2050
|
+
"notesDailyFolder",
|
|
2051
|
+
"notesWikiIncludeFiles",
|
|
2052
|
+
"notesUpgradeV4"
|
|
2053
|
+
];
|
|
2054
|
+
function pickUnitPreferences(input) {
|
|
2055
|
+
return Object.fromEntries(UNIT_PREFERENCE_KEYS.filter((key) => Object.hasOwn(input, key)).map((key) => [key, input[key]]));
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
// unit/accountHttp.ts
|
|
2059
|
+
var digest = (value) => createHash2("sha256").update(JSON.stringify(value)).digest("hex");
|
|
2060
|
+
var AccountChanged = class extends Error {
|
|
2061
|
+
};
|
|
2062
|
+
var preferences = new Set(UNIT_PREFERENCE_KEYS);
|
|
2063
|
+
function createAccountHttp(options) {
|
|
2064
|
+
const writes = /* @__PURE__ */ new Map();
|
|
2065
|
+
const json = (res, status, body) => {
|
|
2066
|
+
res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
2067
|
+
res.end(JSON.stringify(body));
|
|
2068
|
+
};
|
|
2069
|
+
async function read(path) {
|
|
2070
|
+
try {
|
|
2071
|
+
return JSON.parse(await readFile3(path, "utf8"));
|
|
2072
|
+
} catch (error) {
|
|
2073
|
+
if (error.code === "ENOENT") return null;
|
|
2074
|
+
throw error;
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
async function write(path, update, guard) {
|
|
2078
|
+
const job = (writes.get(path) || Promise.resolve()).catch(() => {
|
|
2079
|
+
}).then(async () => {
|
|
2080
|
+
await guard();
|
|
2081
|
+
const value = update(await read(path)), temporary = path + "." + randomUUID3();
|
|
2082
|
+
try {
|
|
2083
|
+
await writeFile(temporary, JSON.stringify(value), { mode: 384 });
|
|
2084
|
+
await guard();
|
|
2085
|
+
await rename(temporary, path);
|
|
2086
|
+
} finally {
|
|
2087
|
+
await rm(temporary, { force: true });
|
|
2088
|
+
}
|
|
2089
|
+
return value;
|
|
2090
|
+
});
|
|
2091
|
+
writes.set(path, job);
|
|
2092
|
+
try {
|
|
2093
|
+
return await job;
|
|
2094
|
+
} finally {
|
|
2095
|
+
if (writes.get(path) === job) writes.delete(path);
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
return async (path, req, res) => {
|
|
2099
|
+
if (path !== "/unit/account" && path !== "/unit/config" && !path.startsWith("/unit/plugin-data/")) return false;
|
|
2100
|
+
const provider = options.provider();
|
|
2101
|
+
if (!provider) return false;
|
|
2102
|
+
const controller = new AbortController();
|
|
2103
|
+
const onClose = () => {
|
|
2104
|
+
if (!res.writableEnded) controller.abort();
|
|
2105
|
+
};
|
|
2106
|
+
res.once("close", onClose);
|
|
2107
|
+
try {
|
|
2108
|
+
const bearer = /^Bearer (\S+)$/i.exec(String(req.headers.authorization || ""))?.[1];
|
|
2109
|
+
const identity = bearer ? await provider.account.resolve(bearer, { signal: controller.signal }) : null;
|
|
2110
|
+
if (!identity) {
|
|
2111
|
+
json(res, 401, { error: "Authentication required" });
|
|
2112
|
+
return true;
|
|
2113
|
+
}
|
|
2114
|
+
if (options.provider()?.account !== provider.account) {
|
|
2115
|
+
json(res, 503, { error: "Account provider changed" });
|
|
2116
|
+
return true;
|
|
2117
|
+
}
|
|
2118
|
+
if (path === "/unit/account") {
|
|
2119
|
+
json(res, req.method === "GET" ? 200 : 405, req.method === "GET" ? identity : { error: "Method not allowed" });
|
|
2120
|
+
return true;
|
|
2121
|
+
}
|
|
2122
|
+
const plugin = path.startsWith("/unit/plugin-data/") ? path.slice("/unit/plugin-data/".length) : null;
|
|
2123
|
+
if (plugin !== null && (!/^[a-zA-Z0-9_-]+$/.test(plugin) || !options.pluginActive(plugin))) {
|
|
2124
|
+
json(res, 404, { error: "Plugin is not active" });
|
|
2125
|
+
return true;
|
|
2126
|
+
}
|
|
2127
|
+
if (req.method !== "GET" && req.method !== "PUT") {
|
|
2128
|
+
json(res, 405, { error: "Method not allowed" });
|
|
2129
|
+
return true;
|
|
2130
|
+
}
|
|
2131
|
+
const dir = join(options.dataDir, "accounts", digest([provider.id, identity.userId, identity.tenantId, identity.workspaceId]));
|
|
2132
|
+
const file2 = join(dir, plugin === null ? "preferences.json" : digest(plugin) + ".json");
|
|
2133
|
+
if (req.method === "GET") {
|
|
2134
|
+
const value2 = await read(file2);
|
|
2135
|
+
json(res, 200, plugin === null ? { config: value2 || {} } : { data: value2 });
|
|
2136
|
+
return true;
|
|
2137
|
+
}
|
|
2138
|
+
let size = 0;
|
|
2139
|
+
const chunks = [];
|
|
2140
|
+
for await (const chunk of req) {
|
|
2141
|
+
size += Buffer.byteLength(chunk);
|
|
2142
|
+
if (size > 1024 * 1024) {
|
|
2143
|
+
json(res, 413, { error: "Data exceeds 1 MB" });
|
|
2144
|
+
return true;
|
|
2145
|
+
}
|
|
2146
|
+
chunks.push(Buffer.from(chunk));
|
|
2147
|
+
}
|
|
2148
|
+
let input;
|
|
2149
|
+
try {
|
|
2150
|
+
input = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
2151
|
+
} catch {
|
|
2152
|
+
json(res, 400, { error: "Invalid JSON" });
|
|
2153
|
+
return true;
|
|
2154
|
+
}
|
|
2155
|
+
if (!input || typeof input !== "object" || Array.isArray(input) || plugin !== null && typeof input.data !== "string") {
|
|
2156
|
+
json(res, 400, { error: "Invalid data" });
|
|
2157
|
+
return true;
|
|
2158
|
+
}
|
|
2159
|
+
const guard = async () => {
|
|
2160
|
+
controller.signal.throwIfAborted();
|
|
2161
|
+
const current = await provider.account.resolve(bearer, { signal: controller.signal });
|
|
2162
|
+
if (!current || digest(current) !== digest(identity) || options.provider()?.account !== provider.account) {
|
|
2163
|
+
throw new AccountChanged("Account changed");
|
|
2164
|
+
}
|
|
2165
|
+
};
|
|
2166
|
+
await guard();
|
|
2167
|
+
await mkdir(dir, { recursive: true, mode: 448 });
|
|
2168
|
+
const value = await write(file2, (previous) => plugin === null ? { ...previous, ...Object.fromEntries(Object.entries(input).filter(([key]) => preferences.has(key))) } : input.data, guard);
|
|
2169
|
+
json(res, 200, plugin === null ? { config: value } : { ok: true });
|
|
2170
|
+
return true;
|
|
2171
|
+
} catch (error) {
|
|
2172
|
+
if (!res.headersSent && !res.destroyed) json(
|
|
2173
|
+
res,
|
|
2174
|
+
error instanceof AccountChanged ? 401 : 503,
|
|
2175
|
+
{ error: error instanceof AccountChanged ? "Account changed" : "Account service unavailable" }
|
|
2176
|
+
);
|
|
2177
|
+
return true;
|
|
2178
|
+
} finally {
|
|
2179
|
+
res.off("close", onClose);
|
|
2180
|
+
}
|
|
2181
|
+
};
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
// unit/localWorkspace.ts
|
|
2185
|
+
import { randomBytes as randomBytes2, createHash as createHash3, randomUUID as randomUUID4 } from "node:crypto";
|
|
2186
|
+
import { readFile as readFile4, writeFile as writeFile2, rename as rename2, mkdir as mkdir2, realpath as realpath3, readdir as readdir2, stat as stat2 } from "node:fs/promises";
|
|
2187
|
+
import { resolve as resolve2, sep as sep3, extname as extname2 } from "node:path";
|
|
2188
|
+
async function localOwnerToken(dataDir) {
|
|
2189
|
+
await mkdir2(dataDir, { recursive: true, mode: 448 });
|
|
2190
|
+
const path = resolve2(dataDir, "owner-token");
|
|
2191
|
+
try {
|
|
2192
|
+
await writeFile2(path, randomBytes2(32).toString("hex"), { mode: 384, flag: "wx" });
|
|
2193
|
+
} catch (error) {
|
|
2194
|
+
if (error.code !== "EEXIST") throw error;
|
|
2195
|
+
}
|
|
2196
|
+
const token = (await readFile4(path, "utf8")).trim();
|
|
2197
|
+
if (!/^[a-f0-9]{64}$/.test(token)) throw new Error("Invalid local workspace owner token");
|
|
2198
|
+
return token;
|
|
2199
|
+
}
|
|
2200
|
+
async function createLocalWorkspace(dataDir, workspaceDir, vaultRoot) {
|
|
2201
|
+
await mkdir2(workspaceDir, { recursive: true, mode: 448 });
|
|
2202
|
+
const token = await localOwnerToken(dataDir);
|
|
2203
|
+
const device = { id: "local-owner", name: "Workspace owner", tokenHash: createHash3("sha256").update(token).digest("hex"), createdAt: 0 };
|
|
2204
|
+
const file2 = resolve2(dataDir, "preferences.json");
|
|
2205
|
+
const read = async () => {
|
|
2206
|
+
try {
|
|
2207
|
+
return pickUnitPreferences(JSON.parse(await readFile4(file2, "utf8")));
|
|
2208
|
+
} catch (error) {
|
|
2209
|
+
if (error.code === "ENOENT") return {};
|
|
2210
|
+
throw error;
|
|
2211
|
+
}
|
|
2212
|
+
};
|
|
2213
|
+
let writes = Promise.resolve();
|
|
2214
|
+
const scoped = async (path) => {
|
|
2215
|
+
try {
|
|
2216
|
+
const actual = await realpath3(path);
|
|
2217
|
+
for (const root of [workspaceDir, vaultRoot()].filter(Boolean)) {
|
|
2218
|
+
const realRoot = await realpath3(root);
|
|
2219
|
+
if (actual === realRoot || actual.startsWith(realRoot + sep3)) return actual;
|
|
2220
|
+
}
|
|
2221
|
+
} catch {
|
|
2222
|
+
}
|
|
2223
|
+
return null;
|
|
2224
|
+
};
|
|
2225
|
+
return {
|
|
2226
|
+
token,
|
|
2227
|
+
device,
|
|
2228
|
+
readConfig: async () => ({ ...await read(), homeDir: workspaceDir, defaultWorkspaceDir: workspaceDir }),
|
|
2229
|
+
writeConfig: async (patch) => {
|
|
2230
|
+
const job = writes.then(async () => {
|
|
2231
|
+
const next = { ...await read(), ...pickUnitPreferences(patch) };
|
|
2232
|
+
const tmp = `${file2}.${randomUUID4()}.tmp`;
|
|
2233
|
+
await writeFile2(tmp, JSON.stringify(next), { mode: 384 });
|
|
2234
|
+
await rename2(tmp, file2);
|
|
2235
|
+
});
|
|
2236
|
+
writes = job.catch(() => {
|
|
2237
|
+
});
|
|
2238
|
+
await job;
|
|
2239
|
+
return { ...await read(), homeDir: workspaceDir, defaultWorkspaceDir: workspaceDir };
|
|
2240
|
+
},
|
|
2241
|
+
readHostFile: async (path, maxBytes = 50 * 1024 * 1024) => {
|
|
2242
|
+
const real = await scoped(path);
|
|
2243
|
+
if (!real) return null;
|
|
2244
|
+
const st = await stat2(real);
|
|
2245
|
+
if (!st.isFile()) return null;
|
|
2246
|
+
const types = { ".md": "text/markdown", ".txt": "text/plain", ".json": "application/json", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".svg": "image/svg+xml", ".pdf": "application/pdf", ".mp4": "video/mp4", ".mp3": "audio/mpeg", ".wav": "audio/wav" };
|
|
2247
|
+
const mimeType = types[extname2(real).toLowerCase()] || "application/octet-stream";
|
|
2248
|
+
if (st.size > Math.min(maxBytes, 50 * 1024 * 1024)) return { mimeType, content: "", size: st.size, mtimeMs: st.mtimeMs, tooLarge: true };
|
|
2249
|
+
return { mimeType, content: (await readFile4(real)).toString("base64"), size: st.size, mtimeMs: st.mtimeMs };
|
|
2250
|
+
},
|
|
2251
|
+
readHostDir: async (path) => {
|
|
2252
|
+
const real = await scoped(path);
|
|
2253
|
+
if (!real || !(await stat2(real)).isDirectory()) return null;
|
|
2254
|
+
const entries = await readdir2(real, { withFileTypes: true });
|
|
2255
|
+
const values = await Promise.all(entries.slice(0, 2e3).map(async (e) => {
|
|
2256
|
+
const child = await scoped(resolve2(real, e.name));
|
|
2257
|
+
if (!child) return null;
|
|
2258
|
+
const st = await stat2(child);
|
|
2259
|
+
return { name: e.name, isDir: st.isDirectory(), size: st.size, path: child };
|
|
2260
|
+
}));
|
|
2261
|
+
return values.filter((e) => !!e).sort((a, b) => Number(b.isDir) - Number(a.isDir) || a.name.localeCompare(b.name));
|
|
2262
|
+
},
|
|
2263
|
+
readHostStat: async (path) => {
|
|
2264
|
+
const real = await scoped(path);
|
|
2265
|
+
if (!real) return null;
|
|
2266
|
+
const st = await stat2(real);
|
|
2267
|
+
return { isDir: st.isDirectory(), mtimeMs: st.mtimeMs, birthtimeMs: st.birthtimeMs || null };
|
|
2268
|
+
}
|
|
2269
|
+
};
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
// unit/host.ts
|
|
2273
|
+
var matches = (path, prefix) => prefix === "/" || path === prefix || path.startsWith(prefix + "/");
|
|
2274
|
+
var pathname = (url) => (url || "/").split("?")[0];
|
|
2275
|
+
async function startBasicUnit(config) {
|
|
2276
|
+
if (!config.instanceId || !config.name || !config.version) throw new Error("Unit identity is required");
|
|
2277
|
+
await readFile5(resolve3(config.webDist, "index.html"));
|
|
2278
|
+
const dataDir = resolve3(config.dataDir || resolve3(homedir(), ".forsion", "units", config.instanceId));
|
|
2279
|
+
await mkdir3(dataDir, { recursive: true, mode: 448 });
|
|
2280
|
+
const workerFile = config.workerFile || fileURLToPath(new URL("./backendWorker.mjs", import.meta.url));
|
|
2281
|
+
if (config.workspace && config.workspace.mode !== "local") throw new Error("Invalid workspace mode");
|
|
2282
|
+
const local = config.workspace?.mode === "local";
|
|
2283
|
+
const workspaceDir = resolve3(config.workspace?.path || resolve3(dataDir, "workspace"));
|
|
2284
|
+
const records = /* @__PURE__ */ new Map();
|
|
2285
|
+
const runtimeVault = () => [...records.values()].find((r) => usable(r) && r.runtime?.vault)?.runtime?.vault || null;
|
|
2286
|
+
const runtimeEngine = () => [...records.values()].find((r) => usable(r) && r.runtime?.engine)?.runtime?.engine;
|
|
2287
|
+
const localWorkspace = local ? await createLocalWorkspace(dataDir, workspaceDir, () => runtimeVault()?.root() || null) : null;
|
|
2288
|
+
const prefix = config.basePath.replace(/\/$/, "") || "/";
|
|
2289
|
+
const protectedPaths = ["unit", "vault", "engine"].map((name) => (prefix === "/" ? "" : prefix) + "/" + name);
|
|
2290
|
+
for (const entry of config.plugins) {
|
|
2291
|
+
const installation = typeof entry === "string" ? { path: entry } : { ...entry };
|
|
2292
|
+
const pack = await readPackage(installation.path, config.version);
|
|
2293
|
+
if (records.has(pack.manifest.id)) throw new Error("Duplicate plugin id");
|
|
2294
|
+
records.set(pack.manifest.id, { package: pack, installation, state: "disabled", mounts: [] });
|
|
2295
|
+
}
|
|
2296
|
+
const ordered = [], spaceOwners = /* @__PURE__ */ new Set();
|
|
2297
|
+
const mustGet = (id) => {
|
|
2298
|
+
const r = records.get(id);
|
|
2299
|
+
if (!r) throw new Error(`Plugin is not installed: ${id}`);
|
|
2300
|
+
return r;
|
|
2301
|
+
};
|
|
2302
|
+
const dependencyOrder = (replacement) => {
|
|
2303
|
+
const order = [], visiting = /* @__PURE__ */ new Set();
|
|
2304
|
+
const visit = (id) => {
|
|
2305
|
+
if (order.includes(id)) return;
|
|
2306
|
+
if (visiting.has(id)) throw new Error("Cyclic plugin dependency");
|
|
2307
|
+
visiting.add(id);
|
|
2308
|
+
const pack = replacement?.id === id ? replacement.package : mustGet(id).package;
|
|
2309
|
+
for (const dep of pack.manifest.requires || []) visit(dep);
|
|
2310
|
+
visiting.delete(id);
|
|
2311
|
+
order.push(id);
|
|
2312
|
+
};
|
|
2313
|
+
for (const id of records.keys()) visit(id);
|
|
2314
|
+
return order;
|
|
2315
|
+
};
|
|
2316
|
+
ordered.push(...dependencyOrder());
|
|
2317
|
+
for (const record of records.values()) for (const id of record.package.spaceIds) {
|
|
2318
|
+
if (spaceOwners.has(id)) throw new Error("Duplicate Space id");
|
|
2319
|
+
spaceOwners.add(id);
|
|
2320
|
+
}
|
|
2321
|
+
if (config.defaultSpace && !spaceOwners.has(config.defaultSpace)) throw new Error("The default Space must be supplied by an installed plugin");
|
|
2322
|
+
const validateFeatures = (replacement) => {
|
|
2323
|
+
const packs = [...records.entries()].map(([id, rec]) => replacement?.id === id ? replacement.package : rec.package);
|
|
2324
|
+
const features = new Set(packs.flatMap((p) => p.manifest.frontend?.features || []));
|
|
2325
|
+
if (features.has("calendar") && !features.has("amadeus")) throw new Error("Calendar requires the Amadeus frontend");
|
|
2326
|
+
if (features.has("automation") && !features.has("tangu")) throw new Error("Automation requires the Tangu frontend");
|
|
2327
|
+
for (const pack of packs) {
|
|
2328
|
+
const owns = pack.manifest.frontend?.features || [];
|
|
2329
|
+
for (const [feature, dependency] of [["calendar", "amadeus"], ["automation", "tangu"]]) {
|
|
2330
|
+
if (!owns.includes(feature) || owns.includes(dependency)) continue;
|
|
2331
|
+
const owner = packs.find((p) => p.manifest.frontend?.features.includes(dependency));
|
|
2332
|
+
if (!pack.manifest.requires?.includes(owner.manifest.id)) throw new Error(`${feature} must require the ${dependency} package`);
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
};
|
|
2336
|
+
validateFeatures();
|
|
2337
|
+
const usable = (rec) => rec.state === "active" && (rec.package.manifest.requires || []).every((id) => usable(mustGet(id)));
|
|
2338
|
+
const visible = (rec) => usable(rec) && rec.installation.publish !== false && (rec.package.manifest.requires || []).every((id) => !mustGet(id).package.manifest.frontend || visible(mustGet(id)));
|
|
2339
|
+
const published = () => [...records.values()].filter(visible);
|
|
2340
|
+
const product = resolveProduct(void 0, { ...basic_default, market: false, onboarding: false, nativeFeatures: [] });
|
|
2341
|
+
const capabilities = () => {
|
|
2342
|
+
const services = {};
|
|
2343
|
+
if (local) return services;
|
|
2344
|
+
for (const rec of published()) Object.assign(services, rec.package.manifest.frontend?.services);
|
|
2345
|
+
const account = [...records.values()].find((r) => usable(r) && r.backend?.account)?.backend?.account;
|
|
2346
|
+
if (!account?.metadata) return {};
|
|
2347
|
+
for (const [id, service] of Object.entries(services)) {
|
|
2348
|
+
const base = account.metadata.apiBase.replace(/\/$/, "");
|
|
2349
|
+
if (service.apiBase !== base && !service.apiBase.startsWith(base + "/")) delete services[id];
|
|
2350
|
+
}
|
|
2351
|
+
return services;
|
|
2352
|
+
};
|
|
2353
|
+
const refreshProduct = () => {
|
|
2354
|
+
const visible2 = published();
|
|
2355
|
+
const spaces = visible2.flatMap((r) => [...r.package.spaceIds]);
|
|
2356
|
+
product.nativeFeatures = visible2.flatMap((r) => r.package.manifest.frontend?.features || []);
|
|
2357
|
+
product.agentBackend = product.nativeFeatures.includes("tangu");
|
|
2358
|
+
product.spaces = spaces.length ? [] : ["home"];
|
|
2359
|
+
product.defaultSpace = spaces.includes(config.defaultSpace || "") ? config.defaultSpace : spaces[0] || "home";
|
|
2360
|
+
};
|
|
2361
|
+
let chain = Promise.resolve(), closing = false, started = false;
|
|
2362
|
+
const syncRuntimes = async () => {
|
|
2363
|
+
const roots = [...records.values()].filter(usable).map((r) => r.package.root);
|
|
2364
|
+
for (const rec of records.values()) if (usable(rec)) await rec.runtime?.setPackages?.(roots, [...records.values()].map((r) => r.package.root));
|
|
2365
|
+
};
|
|
2366
|
+
const serialize = (action2) => {
|
|
2367
|
+
const job = chain.then(action2);
|
|
2368
|
+
chain = job.then(() => {
|
|
2369
|
+
}, () => {
|
|
2370
|
+
});
|
|
2371
|
+
return job;
|
|
2372
|
+
};
|
|
2373
|
+
const bindAddress = config.bindHost === "::" ? "::1" : config.bindHost === "0.0.0.0" || !config.bindHost ? "127.0.0.1" : config.bindHost;
|
|
2374
|
+
const localHost = bindAddress.includes(":") ? `[${bindAddress}]` : bindAddress;
|
|
2375
|
+
const options = (id, rec) => ({
|
|
2376
|
+
id,
|
|
2377
|
+
entry: rec.package.backendEntry,
|
|
2378
|
+
packageDir: rec.package.root,
|
|
2379
|
+
dataDir: resolve3(dataDir, "plugins", id),
|
|
2380
|
+
config: rec.installation.config || {},
|
|
2381
|
+
env: {
|
|
2382
|
+
...rec.installation.config?.env || {},
|
|
2383
|
+
...rec.installation.env,
|
|
2384
|
+
PORT: String(web.port),
|
|
2385
|
+
UNIT_HTTP_ORIGIN: `http://${localHost}:${web.port}`
|
|
2386
|
+
},
|
|
2387
|
+
workerFile,
|
|
2388
|
+
startTimeoutMs: 12e4,
|
|
2389
|
+
onLog: (message) => console.log(`[plugin:${id}] ${message}`),
|
|
2390
|
+
onRestart: () => {
|
|
2391
|
+
if (!closing) void serialize(() => restart(id)).catch(() => console.error(`[unit] Plugin ${id} restart failed`));
|
|
2392
|
+
},
|
|
2393
|
+
onFailure: (error) => {
|
|
2394
|
+
console.error(`[unit] Plugin ${id}: ${error.message}`);
|
|
2395
|
+
rec.state = "failed";
|
|
2396
|
+
rec.backend = void 0;
|
|
2397
|
+
refreshProduct();
|
|
2398
|
+
}
|
|
2399
|
+
});
|
|
2400
|
+
async function activate(id) {
|
|
2401
|
+
if (closing) throw new Error("Unit is stopping");
|
|
2402
|
+
const rec = mustGet(id);
|
|
2403
|
+
if (rec.state === "active") return;
|
|
2404
|
+
for (const dep of rec.package.manifest.requires || []) if (mustGet(dep).state !== "active") throw new Error(`Plugin dependency is not active: ${dep}`);
|
|
2405
|
+
rec.state = "starting";
|
|
2406
|
+
let backend, runtime;
|
|
2407
|
+
try {
|
|
2408
|
+
if (local && rec.package.runtimeEntry) {
|
|
2409
|
+
const module = await import(pathToFileURL(rec.package.runtimeEntry).href + "?generation=" + randomUUID5());
|
|
2410
|
+
const factory = module.createRuntime || module.default;
|
|
2411
|
+
if (typeof factory !== "function") throw new Error("Local runtime has no factory");
|
|
2412
|
+
await mkdir3(resolve3(dataDir, "plugins", id), { recursive: true, mode: 448 });
|
|
2413
|
+
runtime = await factory({ packageDir: rec.package.root, dataDir: resolve3(dataDir, "plugins", id), workspaceDir, config: rec.installation.config || {}, log: (m) => console.log(`[plugin:${id}] ${m}`) });
|
|
2414
|
+
if (!runtime || typeof runtime.close !== "function") throw new Error("Invalid local runtime");
|
|
2415
|
+
if (runtime.vault && runtimeVault() || runtime.engine && runtimeEngine()) throw new Error("Local capability already has a provider");
|
|
2416
|
+
}
|
|
2417
|
+
if (rec.package.backendEntry) {
|
|
2418
|
+
await mkdir3(resolve3(dataDir, "plugins", id), { recursive: true, mode: 448 });
|
|
2419
|
+
backend = await startBackend(options(id, rec));
|
|
2420
|
+
if (backend.account && [...records.values()].some((r) => r !== rec && usable(r) && r.backend?.account)) {
|
|
2421
|
+
throw new Error("A Unit can activate only one account authority");
|
|
2422
|
+
}
|
|
2423
|
+
if (rec.state === "failed") throw new Error("Plugin stopped during startup");
|
|
2424
|
+
const mounts = backend.mounts.map((p) => p === "/" ? p : p.replace(/\/$/, ""));
|
|
2425
|
+
for (const mount of mounts) {
|
|
2426
|
+
if (!/^\/(?:[A-Za-z0-9._~-]+\/?)*$/.test(mount) || mount.split("/").includes("..") || mount === prefix || protectedPaths.some((p) => matches(mount, p))) throw new Error("Plugin claims an invalid or reserved route");
|
|
2427
|
+
if ([...records.values()].some((r) => r !== rec && r.state === "active" && r.mounts.includes(mount))) throw new Error("Plugin route conflict");
|
|
2428
|
+
}
|
|
2429
|
+
rec.mounts = [...new Set(mounts)];
|
|
2430
|
+
}
|
|
2431
|
+
rec.backend = backend;
|
|
2432
|
+
rec.runtime = runtime;
|
|
2433
|
+
rec.state = "active";
|
|
2434
|
+
rec.installation.enabled = true;
|
|
2435
|
+
if (started) await syncRuntimes();
|
|
2436
|
+
} catch (error) {
|
|
2437
|
+
rec.state = "failed";
|
|
2438
|
+
rec.backend = void 0;
|
|
2439
|
+
rec.runtime = void 0;
|
|
2440
|
+
try {
|
|
2441
|
+
if (typeof runtime?.close === "function") await runtime.close();
|
|
2442
|
+
} finally {
|
|
2443
|
+
await backend?.stop();
|
|
2444
|
+
}
|
|
2445
|
+
throw error;
|
|
2446
|
+
} finally {
|
|
2447
|
+
refreshProduct();
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
async function deactivate(id, force = false, preserveIntent = false) {
|
|
2451
|
+
const rec = mustGet(id);
|
|
2452
|
+
if (!force && [...records.values()].some((r) => r.state === "active" && r.package.manifest.requires?.includes(id))) throw new Error("Disable dependent plugins first");
|
|
2453
|
+
const backend = rec.backend, runtime = rec.runtime;
|
|
2454
|
+
rec.backend = void 0;
|
|
2455
|
+
rec.runtime = void 0;
|
|
2456
|
+
rec.state = "stopping";
|
|
2457
|
+
refreshProduct();
|
|
2458
|
+
try {
|
|
2459
|
+
try {
|
|
2460
|
+
await runtime?.close();
|
|
2461
|
+
} finally {
|
|
2462
|
+
await backend?.stop();
|
|
2463
|
+
}
|
|
2464
|
+
} finally {
|
|
2465
|
+
rec.state = "disabled";
|
|
2466
|
+
if (!preserveIntent) rec.installation.enabled = false;
|
|
2467
|
+
refreshProduct();
|
|
2468
|
+
if (started && !closing) await syncRuntimes();
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
const activeTree = (id) => {
|
|
2472
|
+
const affected = /* @__PURE__ */ new Set([id]);
|
|
2473
|
+
for (const candidate of ordered) if (mustGet(candidate).package.manifest.requires?.some((dep) => affected.has(dep))) affected.add(candidate);
|
|
2474
|
+
return ordered.filter((candidate) => affected.has(candidate) && (mustGet(candidate).state === "active" || mustGet(candidate).installation.enabled !== false));
|
|
2475
|
+
};
|
|
2476
|
+
async function restart(id) {
|
|
2477
|
+
mustGet(id);
|
|
2478
|
+
const active = activeTree(id);
|
|
2479
|
+
for (const candidate of [...active].reverse()) await deactivate(candidate, false, true);
|
|
2480
|
+
for (const candidate of ordered.filter((candidate2) => candidate2 === id || active.includes(candidate2))) await activate(candidate);
|
|
2481
|
+
}
|
|
2482
|
+
const select = (path) => {
|
|
2483
|
+
if (protectedPaths.some((p) => matches(path, p))) return void 0;
|
|
2484
|
+
const candidates = [...records.values()].filter((r) => usable(r) && r.backend).flatMap((r) => r.mounts.filter((m) => matches(path, m)).map((m) => ({ rec: r, length: m.length }))).sort((a, b) => b.length - a.length);
|
|
2485
|
+
const winner = candidates[0];
|
|
2486
|
+
return winner && (!matches(path, prefix) || winner.length > prefix.length) ? winner.rec.backend : void 0;
|
|
2487
|
+
};
|
|
2488
|
+
refreshProduct();
|
|
2489
|
+
const accountProvider = () => {
|
|
2490
|
+
const record = [...records.entries()].find(([, r]) => usable(r) && r.backend?.account);
|
|
2491
|
+
return record ? { id: record[0], account: record[1].backend.account } : void 0;
|
|
2492
|
+
};
|
|
2493
|
+
const accountHttp = createAccountHttp({
|
|
2494
|
+
dataDir,
|
|
2495
|
+
provider: accountProvider,
|
|
2496
|
+
pluginActive: (id) => {
|
|
2497
|
+
const rec = records.get(id);
|
|
2498
|
+
return !!rec && usable(rec);
|
|
2499
|
+
}
|
|
2500
|
+
});
|
|
2501
|
+
const web = await startUnitWeb({
|
|
2502
|
+
account: local ? void 0 : { metadata: () => accountProvider()?.account.metadata, handle: accountHttp },
|
|
2503
|
+
meta: { instanceId: config.instanceId, name: config.name, version: config.version },
|
|
2504
|
+
projection: { mode: local ? "local" : "public", basePath: config.basePath, product, capabilities, localCapabilities: () => ({ vault: !!runtimeVault(), engine: !!runtimeEngine()?.endpoint().url, host: !!runtimeEngine()?.endpoint().url }) },
|
|
2505
|
+
routeRequest: (req, res) => {
|
|
2506
|
+
const path = pathname(req.url), backend = select(path);
|
|
2507
|
+
if (backend) {
|
|
2508
|
+
backend.handle(req, res);
|
|
2509
|
+
return true;
|
|
2510
|
+
}
|
|
2511
|
+
if ([...records.values()].some((r) => r.package.backendEntry) && !matches(path, prefix)) {
|
|
2512
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
2513
|
+
res.end('{"detail":"No active plugin route"}');
|
|
2514
|
+
return true;
|
|
2515
|
+
}
|
|
2516
|
+
return false;
|
|
2517
|
+
},
|
|
2518
|
+
routeUpgrade: (req, socket, head) => {
|
|
2519
|
+
const backend = select(pathname(req.url));
|
|
2520
|
+
if (!backend?.upgrade) return false;
|
|
2521
|
+
backend.upgrade(req, socket, head);
|
|
2522
|
+
return true;
|
|
2523
|
+
},
|
|
2524
|
+
getEngine: () => runtimeEngine()?.endpoint() || { url: null, token: "" },
|
|
2525
|
+
pairedDevices: { list: () => localWorkspace ? [localWorkspace.device] : [], add: async () => {
|
|
2526
|
+
throw new Error("Pairing is disabled");
|
|
2527
|
+
} },
|
|
2528
|
+
confirmPair: async () => false,
|
|
2529
|
+
readPlugins: async () => published().flatMap((r) => r.package.ui ? [r.package.ui] : []),
|
|
2530
|
+
readSpaces: async () => published().flatMap((r) => r.package.spaces),
|
|
2531
|
+
readConfig: localWorkspace?.readConfig || (async () => ({})),
|
|
2532
|
+
writeConfig: localWorkspace?.writeConfig || (async () => {
|
|
2533
|
+
throw new Error("Host configuration is not published");
|
|
2534
|
+
}),
|
|
2535
|
+
readProviders: async () => {
|
|
2536
|
+
const rec = [...records.values()].find((r) => usable(r) && r.runtime?.readProviders);
|
|
2537
|
+
return await rec?.runtime?.readProviders?.() || [];
|
|
2538
|
+
},
|
|
2539
|
+
readHostFile: localWorkspace?.readHostFile || (async () => null),
|
|
2540
|
+
readHostDir: localWorkspace?.readHostDir || (async () => null),
|
|
2541
|
+
readHostStat: localWorkspace?.readHostStat || (async () => null),
|
|
2542
|
+
webDistDir: () => config.webDist,
|
|
2543
|
+
vault: runtimeVault,
|
|
2544
|
+
log: (message) => console.log(message)
|
|
2545
|
+
}, { port: config.port, bindHost: config.bindHost ?? "127.0.0.1" });
|
|
2546
|
+
for (const id of ordered) if (mustGet(id).installation.enabled !== false) {
|
|
2547
|
+
try {
|
|
2548
|
+
await activate(id);
|
|
2549
|
+
} catch {
|
|
2550
|
+
console.error(`[unit] Plugin ${id} failed to activate; Unit remains available`);
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
started = true;
|
|
2554
|
+
try {
|
|
2555
|
+
await syncRuntimes();
|
|
2556
|
+
} catch (error) {
|
|
2557
|
+
await web.close();
|
|
2558
|
+
await Promise.allSettled([...records.values()].map(async (rec) => {
|
|
2559
|
+
try {
|
|
2560
|
+
await rec.runtime?.close();
|
|
2561
|
+
} finally {
|
|
2562
|
+
await rec.backend?.stop();
|
|
2563
|
+
}
|
|
2564
|
+
}));
|
|
2565
|
+
throw error;
|
|
2566
|
+
}
|
|
2567
|
+
return {
|
|
2568
|
+
...web,
|
|
2569
|
+
status: () => [...records.values()].map((r) => ({ id: r.package.manifest.id, version: r.package.manifest.version, state: r.state })),
|
|
2570
|
+
enable: (id) => serialize(() => activate(id)),
|
|
2571
|
+
disable: (id) => serialize(() => deactivate(id)),
|
|
2572
|
+
restart: (id) => serialize(() => restart(id)),
|
|
2573
|
+
update: (id, path) => serialize(async () => {
|
|
2574
|
+
const rec = mustGet(id), next = await readPackage(path, config.version);
|
|
2575
|
+
if (next.manifest.id !== id) throw new Error("Update changes plugin identity");
|
|
2576
|
+
for (const dep of next.manifest.requires || []) if (mustGet(dep).state !== "active") throw new Error("New dependency is not active");
|
|
2577
|
+
for (const other of records.values()) if (other !== rec) {
|
|
2578
|
+
for (const space of next.spaceIds) if (other.package.spaceIds.has(space)) throw new Error("Duplicate Space id");
|
|
2579
|
+
}
|
|
2580
|
+
const nextOrder = dependencyOrder({ id, package: next });
|
|
2581
|
+
validateFeatures({ id, package: next });
|
|
2582
|
+
if (config.defaultSpace && rec.package.spaceIds.has(config.defaultSpace) && !next.spaceIds.has(config.defaultSpace)) throw new Error("Update removes the Unit default Space");
|
|
2583
|
+
const previous = rec.package, active = activeTree(id);
|
|
2584
|
+
for (const candidate of [...active].reverse()) await deactivate(candidate, false, true);
|
|
2585
|
+
rec.package = next;
|
|
2586
|
+
try {
|
|
2587
|
+
for (const candidate of nextOrder.filter((candidate2) => active.includes(candidate2))) await activate(candidate);
|
|
2588
|
+
rec.installation.path = next.root;
|
|
2589
|
+
ordered.splice(0, ordered.length, ...nextOrder);
|
|
2590
|
+
} catch (error) {
|
|
2591
|
+
for (const candidate of [...nextOrder].reverse().filter((candidate2) => active.includes(candidate2))) await deactivate(candidate, true, true);
|
|
2592
|
+
rec.package = previous;
|
|
2593
|
+
for (const candidate of active) await activate(candidate);
|
|
2594
|
+
throw error;
|
|
2595
|
+
} finally {
|
|
2596
|
+
refreshProduct();
|
|
2597
|
+
}
|
|
2598
|
+
}),
|
|
2599
|
+
migrate: (id) => serialize(async () => {
|
|
2600
|
+
const rec = mustGet(id);
|
|
2601
|
+
if (rec.state === "active") throw new Error("Disable the plugin before migrating");
|
|
2602
|
+
if (!rec.package.backendEntry) throw new Error("Plugin has no backend migration");
|
|
2603
|
+
await migrateBackend(options(id, rec));
|
|
2604
|
+
}),
|
|
2605
|
+
close: () => serialize(async () => {
|
|
2606
|
+
if (closing) return;
|
|
2607
|
+
closing = true;
|
|
2608
|
+
await web.close();
|
|
2609
|
+
const errors = [];
|
|
2610
|
+
for (const id of [...ordered].reverse()) {
|
|
2611
|
+
try {
|
|
2612
|
+
await deactivate(id, true);
|
|
2613
|
+
} catch (error) {
|
|
2614
|
+
errors.push(error);
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
if (errors.length) throw new AggregateError(errors, "Some plugins failed to stop cleanly");
|
|
2618
|
+
})
|
|
2619
|
+
};
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
// unit/config.ts
|
|
2623
|
+
import { readFile as readFile6, writeFile as writeFile3, rename as rename3, mkdir as mkdir4 } from "node:fs/promises";
|
|
2624
|
+
import { dirname, resolve as resolve4 } from "node:path";
|
|
2625
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
2626
|
+
async function readConfig(file2) {
|
|
2627
|
+
const path = resolve4(file2), root = dirname(path);
|
|
2628
|
+
const config = JSON.parse(await readFile6(path, "utf8"));
|
|
2629
|
+
if (!Array.isArray(config.plugins)) throw new Error("unit.json plugins must be an array");
|
|
2630
|
+
config.webDist = resolve4(root, config.webDist);
|
|
2631
|
+
config.dataDir = resolve4(root, config.dataDir || "data");
|
|
2632
|
+
if (config.workspace?.path) config.workspace.path = resolve4(root, config.workspace.path);
|
|
2633
|
+
if (config.workerFile) config.workerFile = resolve4(root, config.workerFile);
|
|
2634
|
+
config.plugins = config.plugins.map((p) => typeof p === "string" ? { path: resolve4(root, p) } : { ...p, path: resolve4(root, p.path) });
|
|
2635
|
+
return config;
|
|
2636
|
+
}
|
|
2637
|
+
async function writeConfig(file2, config) {
|
|
2638
|
+
await mkdir4(dirname(resolve4(file2)), { recursive: true, mode: 448 });
|
|
2639
|
+
const temp = `${file2}.${randomUUID6()}.tmp`;
|
|
2640
|
+
await writeFile3(temp, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
|
|
2641
|
+
await rename3(temp, file2);
|
|
2642
|
+
}
|
|
2643
|
+
var installations = (config) => config.plugins;
|
|
2644
|
+
|
|
2645
|
+
// unit/install.ts
|
|
2646
|
+
import { cp, mkdir as mkdir6, readdir as readdir3, realpath as realpath4, rm as rm3, lstat, open, readlink, symlink } from "node:fs/promises";
|
|
2647
|
+
import { dirname as dirname3, resolve as resolve6, sep as sep4, relative, isAbsolute as isAbsolute3 } from "node:path";
|
|
2648
|
+
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
2649
|
+
import { spawn } from "node:child_process";
|
|
2650
|
+
|
|
2651
|
+
// unit/control.ts
|
|
2652
|
+
import net from "node:net";
|
|
2653
|
+
import { chmod, rm as rm2, mkdir as mkdir5 } from "node:fs/promises";
|
|
2654
|
+
import { resolve as resolve5, dirname as dirname2 } from "node:path";
|
|
2655
|
+
import { tmpdir } from "node:os";
|
|
2656
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
2657
|
+
var controlAddress = (dataDir) => process.platform === "win32" ? `\\\\.\\pipe\\forsion-unit-${createHash4("sha256").update(resolve5(dataDir)).digest("hex").slice(0, 24)}` : resolve5(tmpdir(), `forsion-unit-${process.getuid?.() ?? "user"}`, createHash4("sha256").update(resolve5(dataDir)).digest("hex").slice(0, 24) + ".sock");
|
|
2658
|
+
async function sendControl(dataDir, command) {
|
|
2659
|
+
return new Promise((resolveReply, reject) => {
|
|
2660
|
+
const socket = net.connect(controlAddress(dataDir));
|
|
2661
|
+
let body = "";
|
|
2662
|
+
socket.setTimeout(24e4, () => socket.destroy(new Error("Unit control timed out")));
|
|
2663
|
+
socket.once("connect", () => socket.write(JSON.stringify(command) + "\n"));
|
|
2664
|
+
socket.on("data", (data) => {
|
|
2665
|
+
body += data;
|
|
2666
|
+
if (body.length > 1e6) {
|
|
2667
|
+
socket.destroy(new Error("Oversized control response"));
|
|
2668
|
+
return;
|
|
2669
|
+
}
|
|
2670
|
+
if (!body.includes("\n")) return;
|
|
2671
|
+
try {
|
|
2672
|
+
const reply = JSON.parse(body.split("\n")[0]);
|
|
2673
|
+
socket.end();
|
|
2674
|
+
if (reply.ok) resolveReply(reply.result);
|
|
2675
|
+
else reject(new Error(reply.error || "Unit command failed"));
|
|
2676
|
+
} catch (error) {
|
|
2677
|
+
socket.destroy();
|
|
2678
|
+
reject(error);
|
|
2679
|
+
}
|
|
2680
|
+
});
|
|
2681
|
+
socket.once("error", reject);
|
|
2682
|
+
socket.once("end", () => {
|
|
2683
|
+
if (!body.includes("\n")) reject(new Error("Unit control connection closed"));
|
|
2684
|
+
});
|
|
2685
|
+
});
|
|
2686
|
+
}
|
|
2687
|
+
var unitIsOffline = (error) => ["ENOENT", "ECONNREFUSED"].includes(error?.code || "");
|
|
2688
|
+
async function startControl(dataDir, dispatch) {
|
|
2689
|
+
await mkdir5(dataDir, { recursive: true, mode: 448 });
|
|
2690
|
+
try {
|
|
2691
|
+
await sendControl(dataDir, { action: "status" });
|
|
2692
|
+
throw new Error("This Unit is already running");
|
|
2693
|
+
} catch (error) {
|
|
2694
|
+
if (!unitIsOffline(error)) throw error;
|
|
2695
|
+
}
|
|
2696
|
+
const address = controlAddress(dataDir);
|
|
2697
|
+
if (process.platform !== "win32") {
|
|
2698
|
+
await mkdir5(dirname2(address), { recursive: true, mode: 448 });
|
|
2699
|
+
await chmod(dirname2(address), 448);
|
|
2700
|
+
}
|
|
2701
|
+
if (process.platform !== "win32") await rm2(address, { force: true });
|
|
2702
|
+
const clients = /* @__PURE__ */ new Set();
|
|
2703
|
+
const server = net.createServer((socket) => {
|
|
2704
|
+
clients.add(socket);
|
|
2705
|
+
socket.once("close", () => clients.delete(socket));
|
|
2706
|
+
let body = "", dispatched = false;
|
|
2707
|
+
socket.setTimeout(24e4, () => socket.destroy());
|
|
2708
|
+
socket.on("data", (data) => {
|
|
2709
|
+
if (dispatched) return;
|
|
2710
|
+
body += data;
|
|
2711
|
+
if (body.length > 1e6) {
|
|
2712
|
+
socket.destroy();
|
|
2713
|
+
return;
|
|
2714
|
+
}
|
|
2715
|
+
if (!body.includes("\n")) return;
|
|
2716
|
+
dispatched = true;
|
|
2717
|
+
void (async () => {
|
|
2718
|
+
try {
|
|
2719
|
+
socket.end(JSON.stringify({ ok: true, result: await dispatch(JSON.parse(body.split("\n")[0])) }) + "\n");
|
|
2720
|
+
} catch (error) {
|
|
2721
|
+
socket.end(JSON.stringify({ ok: false, error: error instanceof Error ? error.message : "Unit command failed" }) + "\n");
|
|
2722
|
+
}
|
|
2723
|
+
})();
|
|
2724
|
+
});
|
|
2725
|
+
socket.on("error", () => {
|
|
2726
|
+
});
|
|
2727
|
+
});
|
|
2728
|
+
await new Promise((ready, reject) => {
|
|
2729
|
+
server.once("error", reject);
|
|
2730
|
+
server.listen(address, ready);
|
|
2731
|
+
});
|
|
2732
|
+
if (process.platform !== "win32") await chmod(address, 384);
|
|
2733
|
+
return { close: async () => {
|
|
2734
|
+
await new Promise((done) => {
|
|
2735
|
+
server.close(() => done());
|
|
2736
|
+
for (const socket of clients) socket.destroy();
|
|
2737
|
+
});
|
|
2738
|
+
if (process.platform !== "win32") await rm2(address, { force: true });
|
|
2739
|
+
} };
|
|
2740
|
+
}
|
|
2741
|
+
|
|
2742
|
+
// unit/install.ts
|
|
2743
|
+
async function inspectTree(dir, root, seen = /* @__PURE__ */ new Set()) {
|
|
2744
|
+
const actual = await realpath4(dir);
|
|
2745
|
+
if (seen.has(actual)) return;
|
|
2746
|
+
seen.add(actual);
|
|
2747
|
+
for (const entry of await readdir3(dir, { withFileTypes: true })) {
|
|
2748
|
+
const path = resolve6(dir, entry.name);
|
|
2749
|
+
if (entry.name === ".env" || entry.name.startsWith(".env.") && !entry.name.endsWith(".example")) throw new Error("Package contains private environment files");
|
|
2750
|
+
if (entry.isSymbolicLink()) {
|
|
2751
|
+
const target = await realpath4(path);
|
|
2752
|
+
if (!target.startsWith(root + sep4)) throw new Error("Package symlink escapes its directory");
|
|
2753
|
+
if ((await lstat(target)).isDirectory()) await inspectTree(target, root, seen);
|
|
2754
|
+
} else if (entry.isDirectory()) await inspectTree(path, root, seen);
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
async function relocateLinks(dir, source, destination) {
|
|
2758
|
+
for (const entry of await readdir3(dir, { withFileTypes: true })) {
|
|
2759
|
+
const path = resolve6(dir, entry.name);
|
|
2760
|
+
if (entry.isSymbolicLink()) {
|
|
2761
|
+
const link = await readlink(path);
|
|
2762
|
+
if (isAbsolute3(link)) {
|
|
2763
|
+
const relocated = resolve6(destination, relative(source, await realpath4(link)));
|
|
2764
|
+
await rm3(path);
|
|
2765
|
+
await symlink(relative(dirname3(path), relocated), path);
|
|
2766
|
+
}
|
|
2767
|
+
} else if (entry.isDirectory()) await relocateLinks(path, source, destination);
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
async function installDependencies(root, backendMain) {
|
|
2771
|
+
const backend = resolve6(root, backendMain.split("/")[0]);
|
|
2772
|
+
await packagePath(root, backendMain.split("/")[0] + "/package-lock.json");
|
|
2773
|
+
await new Promise((done, reject) => {
|
|
2774
|
+
const win = process.platform === "win32";
|
|
2775
|
+
const child = spawn(win ? "cmd.exe" : "npm", win ? ["/c", "npm", "ci", "--omit=dev"] : ["ci", "--omit=dev"], { cwd: backend, stdio: "inherit", env: { ...process.env, PATH: dirname3(process.execPath) + (win ? ";" : ":") + (process.env.PATH || "") } });
|
|
2776
|
+
child.once("error", reject);
|
|
2777
|
+
child.once("exit", (code) => code === 0 ? done() : reject(new Error(`Plugin dependency installation failed (${code})`)));
|
|
2778
|
+
});
|
|
2779
|
+
}
|
|
2780
|
+
async function installPackage(configPath, source) {
|
|
2781
|
+
const file2 = resolve6(configPath), root = dirname3(file2);
|
|
2782
|
+
const lockPath = resolve6(root, ".install.lock");
|
|
2783
|
+
const lock = await open(lockPath, "wx", 384).catch(() => {
|
|
2784
|
+
throw new Error("Another install is in progress");
|
|
2785
|
+
});
|
|
2786
|
+
let stage, committed = false;
|
|
2787
|
+
try {
|
|
2788
|
+
const config = await readConfig(file2), sourceRoot = await realpath4(source);
|
|
2789
|
+
if (!(await lstat(sourceRoot)).isDirectory()) throw new Error("Install expects an unpacked plugin directory");
|
|
2790
|
+
const pack = await readPackage(sourceRoot, config.version);
|
|
2791
|
+
await inspectTree(sourceRoot, sourceRoot);
|
|
2792
|
+
stage = resolve6(root, "plugins", pack.manifest.id, `${pack.manifest.version}-${randomUUID7()}`);
|
|
2793
|
+
await mkdir6(dirname3(stage), { recursive: true, mode: 448 });
|
|
2794
|
+
await cp(sourceRoot, stage, { recursive: true, verbatimSymlinks: true });
|
|
2795
|
+
await relocateLinks(stage, sourceRoot, stage);
|
|
2796
|
+
if (pack.manifest.backend?.dependencies?.mode === "npm-ci") await installDependencies(stage, pack.manifest.backend.main);
|
|
2797
|
+
const ready = await readPackage(stage, config.version);
|
|
2798
|
+
const entries = installations(config);
|
|
2799
|
+
const existing = (await Promise.all(entries.map(async (entry) => ({ entry, pack: await readPackage(entry.path) })))).find((p) => p.pack.manifest.id === pack.manifest.id);
|
|
2800
|
+
if (existing) {
|
|
2801
|
+
try {
|
|
2802
|
+
await sendControl(config.dataDir, { action: "update", id: pack.manifest.id, path: stage });
|
|
2803
|
+
committed = true;
|
|
2804
|
+
return { id: ready.manifest.id, version: ready.manifest.version, path: stage };
|
|
2805
|
+
} catch (error) {
|
|
2806
|
+
if (!unitIsOffline(error)) {
|
|
2807
|
+
committed = true;
|
|
2808
|
+
throw error;
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
existing.entry.path = stage;
|
|
2812
|
+
} else {
|
|
2813
|
+
try {
|
|
2814
|
+
await sendControl(config.dataDir, { action: "status" });
|
|
2815
|
+
throw new Error("Stop the Unit before installing a new plugin; updates can run live");
|
|
2816
|
+
} catch (error) {
|
|
2817
|
+
if (!unitIsOffline(error)) throw error;
|
|
2818
|
+
}
|
|
2819
|
+
entries.push({ path: stage, enabled: true });
|
|
2820
|
+
}
|
|
2821
|
+
if (!config.defaultSpace && ready.spaceIds.size) config.defaultSpace = [...ready.spaceIds][0];
|
|
2822
|
+
await writeConfig(file2, config);
|
|
2823
|
+
committed = true;
|
|
2824
|
+
return { id: ready.manifest.id, version: ready.manifest.version, path: stage };
|
|
2825
|
+
} finally {
|
|
2826
|
+
await lock.close();
|
|
2827
|
+
await rm3(lockPath, { force: true });
|
|
2828
|
+
if (!committed && stage) await rm3(stage, { recursive: true, force: true });
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
// desktop/package.json
|
|
2833
|
+
var package_default = {
|
|
2834
|
+
name: "forsion-desktop",
|
|
2835
|
+
version: "2.12.2",
|
|
2836
|
+
description: "Forsion Desktop \u2014 AI \u9A71\u52A8\u7684\u5E94\u7528\u751F\u6001\u7CFB\u7EDF\u7684\u684C\u9762\u7AEF(Electron):Tangu Agent \u5F15\u64CE + LCL \u53EF\u505C\u9760\u5DE5\u4F5C\u533A\u6846\u67B6 + \u53EF\u63D2\u62D4\u529F\u80FD\u7A7A\u95F4(\u5BF9\u8BDD/\u7B14\u8BB0/\u65E5\u5386/\u6536\u4EF6\u7BB1/\u7F16\u7801),\u6309\u4EA7\u54C1\u6863\u6848\u88C1\u51FA\u5168\u5BB6\u6876\u4E0E\u5355\u54C1\u53D1\u884C\u7248",
|
|
2837
|
+
author: "Forsion",
|
|
2838
|
+
private: true,
|
|
2839
|
+
type: "module",
|
|
2840
|
+
main: "out/main/main.js",
|
|
2841
|
+
scripts: {
|
|
2842
|
+
postinstall: "node build/link-lcl.cjs && node build/copy-excalidraw-assets.cjs && node build/gen-fonts.cjs && node build/fix-pty-helper.cjs",
|
|
2843
|
+
predev: "node scripts/dev-version.check.cjs",
|
|
2844
|
+
dev: "electron-vite dev --noSandbox",
|
|
2845
|
+
"check:visualize": "node scripts/visualize.check.cjs",
|
|
2846
|
+
"check:version": "node scripts/dev-version.check.cjs",
|
|
2847
|
+
build: "electron-vite build",
|
|
2848
|
+
preview: "electron-vite preview",
|
|
2849
|
+
web: "vite frontend",
|
|
2850
|
+
gallery: "echo '\u2192 http://localhost:8100/lcl/gallery/' && python3 -m http.server 8100 --bind 127.0.0.1 --directory ..",
|
|
2851
|
+
test: "npm run check:shadowcontract && vitest run",
|
|
2852
|
+
"check:run-lifecycle": "node scripts/run-lifecycle.check.cjs",
|
|
2853
|
+
"check:installer": "node scripts/installer-compile.check.cjs",
|
|
2854
|
+
"check:backend-deps": "node scripts/backend-dependencies.check.cjs",
|
|
2855
|
+
"test:sync": "vitest run electron/amadeus/ipc.move.test.ts electron/amadeus/ipc.account.test.ts electron/amadeus/settings.account.test.ts frontend/src/amadeus/store/pageStore.move-race.test.ts frontend/src/stores/entrySyncStore frontend/src/services/cloudBridgeVanished.test.ts frontend/src/components/ShareCard.race.test.ts frontend/src/amadeusFolderPublish.test.ts",
|
|
2856
|
+
"test:accounts": "vitest run forsionAuth accountCore accountTransition AccountCard cloudAccount cloudHttpAccount cloudBridgeCacheAccount contentLayoutAccount settings.account ipc.account collabMain.account appStore.connect engine.lifecycle writeFailures pageStore.vaultswitch dbStore.test drawingStore.test",
|
|
2857
|
+
"e2e:automation": "node scripts/automation-space.e2e.cjs",
|
|
2858
|
+
"e2e:editor": "node scripts/e2e-editor.cjs",
|
|
2859
|
+
"e2e:dnd": "node scripts/block-dnd.e2e.cjs",
|
|
2860
|
+
"e2e:sidebardrop": "node scripts/sidebar-drop.e2e.cjs",
|
|
2861
|
+
"e2e:ribbon": "node scripts/ribbon-dnd.e2e.cjs",
|
|
2862
|
+
"e2e:mindmap": "node scripts/mindmap.e2e.cjs",
|
|
2863
|
+
"e2e:canvas": "node scripts/canvas.e2e.cjs",
|
|
2864
|
+
"e2e:daily": "node scripts/daily-template.e2e.cjs",
|
|
2865
|
+
"e2e:plugview": "node scripts/plugin-view.e2e.cjs",
|
|
2866
|
+
"e2e:eventsspace": "node scripts/events-space-switch.repro.cjs",
|
|
2867
|
+
"check:listsrc": "node scripts/plugin-listsrc.check.cjs",
|
|
2868
|
+
"e2e:latex": "node scripts/e2e-editor.cjs --script=latex-suite.e2e.cjs",
|
|
2869
|
+
"e2e:activewindow": "node scripts/active-window.e2e.cjs",
|
|
2870
|
+
"e2e:recall": "node scripts/recall-rewind.e2e.cjs",
|
|
2871
|
+
"e2e:chatevents": "node scripts/chat-events.e2e.cjs",
|
|
2872
|
+
"e2e:computerhistory": "node scripts/computer-history.e2e.cjs",
|
|
2873
|
+
"e2e:remotesafety": "node scripts/remote-safety.e2e.cjs",
|
|
2874
|
+
"e2e:chatshimmer": "node scripts/chat-shimmer.e2e.cjs",
|
|
2875
|
+
"e2e:runstats": "node scripts/chat-runstats.e2e.cjs",
|
|
2876
|
+
"e2e:ctxwindow": "node scripts/ctxwindow-settings.e2e.cjs",
|
|
2877
|
+
"e2e:ctxlimit": "node scripts/ctxlimit-menu.e2e.cjs",
|
|
2878
|
+
"e2e:livewait": "node scripts/chat-livewait.e2e.cjs",
|
|
2879
|
+
"e2e:queue": "node scripts/queued-commands.e2e.cjs",
|
|
2880
|
+
"e2e:musespace": "node scripts/muse-space.e2e.cjs",
|
|
2881
|
+
"e2e:coreplugins": "node scripts/core-plugins.e2e.cjs",
|
|
2882
|
+
"e2e:restart-update": "node scripts/restart-update.e2e.cjs",
|
|
2883
|
+
"e2e:market-submissions": "node scripts/market-submissions.e2e.cjs",
|
|
2884
|
+
"e2e:market-auto-update": "node scripts/market-auto-update.e2e.cjs",
|
|
2885
|
+
"e2e:account": "node scripts/account-center.shot.cjs",
|
|
2886
|
+
"e2e:accountpopups": "node scripts/account-popups.shot.cjs",
|
|
2887
|
+
"e2e:extend": "node scripts/extend-seam.e2e.cjs",
|
|
2888
|
+
"e2e:musesettings": "node scripts/muse-settings.e2e.cjs",
|
|
2889
|
+
"e2e:modelcatalog": "node scripts/model-catalog.e2e.cjs",
|
|
2890
|
+
"e2e:museagentspace": "node scripts/muse-agentspace.e2e.cjs",
|
|
2891
|
+
"e2e:inboxamadeus": "node scripts/inbox-amadeus.e2e.cjs",
|
|
2892
|
+
"e2e:planlive": "node scripts/plan-live.e2e.cjs",
|
|
2893
|
+
"live:harness": "cd ../tangu-agent && npm run live:harness",
|
|
2894
|
+
"probe:gzipupload": "node scripts/gzip-upload.probe.cjs",
|
|
2895
|
+
"e2e:realtimevoice": "node scripts/realtime-voice.e2e.cjs",
|
|
2896
|
+
"e2e:dashdrag": "node scripts/dash3-drag.e2e.cjs",
|
|
2897
|
+
"e2e:pathgone": "node scripts/vault-pathgone.e2e.cjs",
|
|
2898
|
+
"check:pillbar": "node scripts/pillbar-overflow.check.cjs",
|
|
2899
|
+
"check:feedback": "node scripts/feedback.check.cjs",
|
|
2900
|
+
"check:floatingsession": "node scripts/floating-session.check.cjs",
|
|
2901
|
+
"check:btw": "node scripts/btw.check.cjs",
|
|
2902
|
+
"check:chatlayout": "node scripts/chat-layout.check.cjs",
|
|
2903
|
+
"check:sessionemoji": "node scripts/session-emoji.check.cjs",
|
|
2904
|
+
"check:messageactions": "node scripts/chat-message-actions.check.cjs",
|
|
2905
|
+
"check:approval": "node scripts/approval-menu.check.cjs",
|
|
2906
|
+
"check:addmenu": "node scripts/add-menu.check.cjs",
|
|
2907
|
+
"check:caret": "node scripts/smooth-caret.check.cjs",
|
|
2908
|
+
"check:spacerestore": "node scripts/space-restore.check.cjs",
|
|
2909
|
+
"check:spacerestart": "node scripts/space-restart.check.cjs",
|
|
2910
|
+
"check:bluebirdspace": "node scripts/bluebird-space.check.cjs",
|
|
2911
|
+
"check:bluebirdtimestamps": "node scripts/bluebird-space.check.cjs --timestamps",
|
|
2912
|
+
"check:splitregions": "node scripts/split-regions.check.cjs",
|
|
2913
|
+
"check:br": "node scripts/e2e-editor.cjs --check=blank-line-br",
|
|
2914
|
+
"check:computerhistory": "node scripts/e2e-editor.cjs --check=computer-history-settings",
|
|
2915
|
+
"check:browserext": "node scripts/e2e-editor.cjs --check=browserext-settings",
|
|
2916
|
+
"check:attention": "node scripts/e2e-editor.cjs --check=attention-save",
|
|
2917
|
+
"check:rtcorpus": "node scripts/e2e-editor.cjs --check=rtcorpus",
|
|
2918
|
+
"check:cjk": "node scripts/e2e-editor.cjs --check=cjk-flanking",
|
|
2919
|
+
"check:linkcard": "node scripts/e2e-editor.cjs --check=linkcard",
|
|
2920
|
+
"check:pastefidelity": "node scripts/e2e-editor.cjs --check=pastefidelity",
|
|
2921
|
+
"check:templatefm": "node scripts/e2e-editor.cjs --check=template-fm",
|
|
2922
|
+
"check:enter": "node scripts/e2e-editor.cjs --check=enter-semantics",
|
|
2923
|
+
"check:caretmerge": "node scripts/e2e-editor.cjs --check=caret-merge",
|
|
2924
|
+
"check:canvascaret": "node scripts/e2e-editor.cjs --check=canvas-caret",
|
|
2925
|
+
"check:unifiedro": "node scripts/e2e-editor.cjs --check=unified-readonly",
|
|
2926
|
+
"check:unifiedcas": "node scripts/e2e-editor.cjs --check=unifiedcas",
|
|
2927
|
+
"check:spellctx": "node scripts/e2e-editor.cjs --check=unified-spellctx",
|
|
2928
|
+
"check:unifiedkeys": "node scripts/e2e-editor.cjs --check=unified-keys",
|
|
2929
|
+
"check:taskbox": "node scripts/e2e-editor.cjs --check=taskbox",
|
|
2930
|
+
"check:table": "node scripts/e2e-editor.cjs --check=unified-table",
|
|
2931
|
+
"check:unifiedsync": "node scripts/e2e-editor.cjs --check=unified-sync",
|
|
2932
|
+
"check:editorai": "node scripts/e2e-editor.cjs --check=editor-ai",
|
|
2933
|
+
"check:menus": "node scripts/e2e-editor.cjs --check=editor-menus",
|
|
2934
|
+
"check:cloudgap": "node scripts/cloud-gap.check.cjs",
|
|
2935
|
+
"check:amadeusinteractions": "node scripts/e2e-editor.cjs --check=amadeus-interactions",
|
|
2936
|
+
"check:dashboard": "node scripts/e2e-editor.cjs --check=dashboard",
|
|
2937
|
+
"check:embedinputs": "node scripts/e2e-editor.cjs --check=embed-inputs",
|
|
2938
|
+
"check:embedkeep": "node scripts/e2e-editor.cjs --check=embed-keep",
|
|
2939
|
+
"check:outlinereveal": "node scripts/e2e-editor.cjs --check=outline-reveal",
|
|
2940
|
+
"check:coverrepo": "node scripts/e2e-editor.cjs --check=cover-reposition",
|
|
2941
|
+
"check:pagestyle": "node scripts/e2e-editor.cjs --check=page-style",
|
|
2942
|
+
"check:pagehistory": "node scripts/e2e-editor.cjs --check=page-history",
|
|
2943
|
+
"check:mobilebar": "node scripts/e2e-editor.cjs --check=mobile-bar",
|
|
2944
|
+
"check:editorperf": "node scripts/e2e-editor.cjs --check=editor-perf",
|
|
2945
|
+
"check:codelanguage": "node scripts/e2e-editor.cjs --check=code-language",
|
|
2946
|
+
"check:dbinteractions": "node scripts/e2e-editor.cjs --check=db-interactions",
|
|
2947
|
+
"e2e:dbrecords": "node scripts/db-records.e2e.cjs",
|
|
2948
|
+
"e2e:dbvisual": "node scripts/db-records.e2e.cjs --visual",
|
|
2949
|
+
"check:colorder": "node scripts/e2e-editor.cjs --check=db-colorder",
|
|
2950
|
+
"check:viewcols": "node scripts/e2e-editor.cjs --check=db-viewcols",
|
|
2951
|
+
"shot:dashboard": "node scripts/e2e-editor.cjs --check=dashboard --shot",
|
|
2952
|
+
"check:dashgrid": "node scripts/e2e-editor.cjs --check=dashgrid",
|
|
2953
|
+
"check:dashrecipe": "node scripts/e2e-editor.cjs --check=dashrecipe",
|
|
2954
|
+
"check:dashmount": "node scripts/e2e-editor.cjs --check=dashmount",
|
|
2955
|
+
"check:tablemount": "node scripts/e2e-editor.cjs --check=tablemount",
|
|
2956
|
+
"check:erp": "node scripts/e2e-editor.cjs --check=erp",
|
|
2957
|
+
"check:rowlink": "node scripts/rowlink.check.mjs",
|
|
2958
|
+
"e2e:erp": "node scripts/erp-space.e2e.cjs",
|
|
2959
|
+
"check:boot": "node scripts/boot-replay.check.cjs",
|
|
2960
|
+
"shot:dashgrid": "node scripts/e2e-editor.cjs --check=dashgrid --shot",
|
|
2961
|
+
"check:overlay": "node scripts/overlay-zoom.check.cjs",
|
|
2962
|
+
"check:leadicon": "node scripts/t2s-lead-icon.check.cjs",
|
|
2963
|
+
"check:tasksummary": "node scripts/task-summary.check.cjs",
|
|
2964
|
+
"check:plancard": "node scripts/plan-card.check.cjs",
|
|
2965
|
+
"check:searchrow": "node scripts/search-row.check.cjs",
|
|
2966
|
+
"check:diffmulti": "node scripts/diff-multifile.check.cjs",
|
|
2967
|
+
"check:railjank": "node scripts/rail-jank.check.cjs",
|
|
2968
|
+
"check:deskrail": "node scripts/desk-rail.check.cjs",
|
|
2969
|
+
"check:deskcompanion": "node scripts/desk-companion.check.cjs",
|
|
2970
|
+
"e2e:pdfcite": "node scripts/pdf-citation.e2e.cjs",
|
|
2971
|
+
"check:hlband": "node scripts/pdf-highlight-geometry.check.cjs",
|
|
2972
|
+
"e2e:filecite": "node scripts/file-citation.e2e.cjs",
|
|
2973
|
+
"e2e:pathgonefiles": "node scripts/vault-pathgone-files.e2e.cjs",
|
|
2974
|
+
"e2e:pathgoneunmounted": "node scripts/vault-pathgone-unmounted.e2e.cjs",
|
|
2975
|
+
"check:textfrag": "electron scripts/text-fragment.check.cjs",
|
|
2976
|
+
"check:assetfetch": "electron scripts/asset-fetch.check.cjs",
|
|
2977
|
+
"check:rtc": "electron scripts/rtc-capability.check.cjs",
|
|
2978
|
+
"check:rtcsdk": "electron scripts/rtc-sdk.check.cjs",
|
|
2979
|
+
"probe:rtc": "electron scripts/rtc-live.probe.cjs",
|
|
2980
|
+
"e2e:inspect": "node scripts/inspect-overlay.e2e.cjs",
|
|
2981
|
+
"e2e:mediacite": "node scripts/media-citation.e2e.cjs",
|
|
2982
|
+
"e2e:chatembed": "node scripts/chat-embed.e2e.cjs",
|
|
2983
|
+
"e2e:webcite": "node scripts/web-citation.e2e.cjs",
|
|
2984
|
+
"check:ctxring": "node scripts/ctxring.check.cjs",
|
|
2985
|
+
"check:ctxusage": "node scripts/e2e-editor.cjs --check=ctxusage --shot",
|
|
2986
|
+
"check:connect": "node scripts/connect-preview.check.cjs",
|
|
2987
|
+
"check:modelmenu": "node scripts/model-menu.check.cjs",
|
|
2988
|
+
"check:menutap": "node scripts/menu-tap.check.cjs",
|
|
2989
|
+
"check:settingsui": "node scripts/settings-ui.check.cjs",
|
|
2990
|
+
"check:agentmemory": "node scripts/agent-memory.check.cjs",
|
|
2991
|
+
"check:settingsmode": "node scripts/settings-mode.check.cjs",
|
|
2992
|
+
"check:themecontrast": "node scripts/theme-contrast.check.cjs",
|
|
2993
|
+
"check:themevisual": "node scripts/theme-visual.check.cjs",
|
|
2994
|
+
"check:themeapp": "node scripts/theme-app.check.cjs",
|
|
2995
|
+
"check:marketui": "node scripts/market-ui.check.cjs",
|
|
2996
|
+
"probe:marketdl": "node scripts/market-download.probe.cjs",
|
|
2997
|
+
"check:uiscale": "node scripts/ui-scale.check.cjs",
|
|
2998
|
+
"check:uifont": "node scripts/ui-font.check.cjs",
|
|
2999
|
+
"check:topchrome": "node scripts/top-chrome.check.cjs",
|
|
3000
|
+
"check:minicard": "node scripts/mini-card.check.cjs",
|
|
3001
|
+
"check:floatingpanel": "node scripts/floating-panel.check.cjs",
|
|
3002
|
+
"check:quietfocus": "node scripts/quiet-focus.check.cjs",
|
|
3003
|
+
"check:persistignore": "node scripts/persist-ignore.check.cjs",
|
|
3004
|
+
"check:onboarding": "node scripts/plugin-onboarding.check.cjs",
|
|
3005
|
+
"check:plugindeps": "node scripts/plugin-deps.check.cjs",
|
|
3006
|
+
"check:pluginsync": "node scripts/plugin-install-sync.check.cjs",
|
|
3007
|
+
"check:first-run": "node scripts/onboarding.check.cjs",
|
|
3008
|
+
"check:miniauto": "node scripts/mini-auto.check.cjs",
|
|
3009
|
+
"check:board": "node scripts/board-paper.check.cjs",
|
|
3010
|
+
"check:boardlive": "node scripts/e2e-editor.cjs --check=board-live",
|
|
3011
|
+
"check:scrollbar": "node scripts/scrollbar-overlay.check.cjs",
|
|
3012
|
+
"check:callout": "node scripts/callout-fold.check.cjs",
|
|
3013
|
+
"check:toggle": "node scripts/e2e-editor.cjs --check=toggle-block",
|
|
3014
|
+
"check:togglecontent": "node scripts/e2e-editor.cjs --check=toggle-content",
|
|
3015
|
+
"check:togglemotion": "node scripts/e2e-editor.cjs --check=toggle-motion",
|
|
3016
|
+
"check:tabindent": "node scripts/e2e-editor.cjs --check=tab-indent",
|
|
3017
|
+
"check:srcbtn": "node scripts/source-toggle.check.cjs",
|
|
3018
|
+
"check:bsurface": "node scripts/e2e-editor.cjs --check=plugin-blocksurface",
|
|
3019
|
+
"build:unitweb": "node scripts/build-unit-web.mjs",
|
|
3020
|
+
pack: "npm run build:unitweb && electron-builder --config electron-builder.config.cjs --dir",
|
|
3021
|
+
dist: "npm run build:unitweb && electron-builder --config electron-builder.config.cjs",
|
|
3022
|
+
"fetch-python": "node build/fetch-python.cjs",
|
|
3023
|
+
"fetch-node": "node build/fetch-node.cjs",
|
|
3024
|
+
"fetch-office": "node build/fetch-office.cjs",
|
|
3025
|
+
"fetch-git": "node build/fetch-git.cjs",
|
|
3026
|
+
typecheck: "tsc --noEmit && node ../tangu-agent/scripts/sync-command-catalog.mjs --check",
|
|
3027
|
+
"predev:amadeus": "node scripts/dev-version.check.cjs",
|
|
3028
|
+
"dev:amadeus": "FORSION_PRODUCT=amadeus electron-vite dev --noSandbox",
|
|
3029
|
+
"build:amadeus": "FORSION_PRODUCT=amadeus electron-vite build",
|
|
3030
|
+
"pack:amadeus": "FORSION_PRODUCT=amadeus npm run build:amadeus && FORSION_PRODUCT=amadeus electron-builder --config electron-builder.config.cjs --dir",
|
|
3031
|
+
"check:extendview": "node scripts/extend-view.check.cjs",
|
|
3032
|
+
"check:tablegroups": "node scripts/extend-view.check.cjs --groups-only",
|
|
3033
|
+
"check:bottompanel": "node scripts/bottom-panel.check.cjs",
|
|
3034
|
+
"check:bottomiframe": "node scripts/bottom-toggle-iframe.electron.cjs",
|
|
3035
|
+
"shot:bottompanel": "node scripts/bottom-panel.check.cjs --shot",
|
|
3036
|
+
"check:remount": "node scripts/main-remount.check.cjs",
|
|
3037
|
+
"check:nav": "node scripts/nav-history.check.cjs",
|
|
3038
|
+
"check:builtins": "node scripts/builtins.check.cjs",
|
|
3039
|
+
"check:runblock": "node scripts/runblock.check.cjs",
|
|
3040
|
+
"live:runblock": "node scripts/runblock-live.e2e.cjs",
|
|
3041
|
+
"check:suggest": "node scripts/suggest-chip.check.cjs",
|
|
3042
|
+
"check:hooks": 'eslint --config build/eslint.hooks.mjs "frontend/src/**/*.{ts,tsx}"',
|
|
3043
|
+
"check:deskshot": "node scripts/desk-shot.check.cjs",
|
|
3044
|
+
"check:splash": "node scripts/splash-loop.check.cjs",
|
|
3045
|
+
"check:parity": "node scripts/platform-parity.check.cjs",
|
|
3046
|
+
"check:enginetarget": "node scripts/engine-target.check.cjs",
|
|
3047
|
+
"check:remotechain": "node scripts/remote-chain.e2e.cjs",
|
|
3048
|
+
"check:streamrenew": "node scripts/stream-renew.check.cjs",
|
|
3049
|
+
"check:phonedist": "node scripts/lib/phone-dist-stamp.selftest.cjs",
|
|
3050
|
+
"check:unitswitcher": "node scripts/unit-switcher.check.cjs",
|
|
3051
|
+
"check:secrets": "node scripts/device-secrets.e2e.cjs",
|
|
3052
|
+
"gen:engineroutes": "node scripts/gen-engine-routes.mjs",
|
|
3053
|
+
"test:ics-tz": 'for tz in Europe/London America/New_York Asia/Shanghai Australia/Lord_Howe UTC; do echo "== $tz"; TZ=$tz vitest run frontend/src/views/calendar/ics.test.ts || exit 1; done',
|
|
3054
|
+
"check:arrowtrap": "node scripts/arrow-trap.check.cjs",
|
|
3055
|
+
"check:wikisuggest": "node scripts/e2e-editor.cjs --check=wiki-suggest",
|
|
3056
|
+
"check:qfentry": "node scripts/e2e-editor.cjs --check=quickfind-entry",
|
|
3057
|
+
"check:bsfocus": "node scripts/backspace-focus.check.cjs",
|
|
3058
|
+
"check:docs": "node scripts/docs-links.check.cjs",
|
|
3059
|
+
"check:menuclamp": "node scripts/menu-clamp.check.cjs",
|
|
3060
|
+
"check:capsule": "node scripts/mobile-capsule.check.cjs",
|
|
3061
|
+
"check:drawer": "node scripts/mobile-drawer.check.cjs",
|
|
3062
|
+
"check:uimode": "node scripts/uimode-auto.check.cjs",
|
|
3063
|
+
"check:focusring": "node scripts/focus-ring.check.cjs",
|
|
3064
|
+
"check:layoutreset": "node scripts/layout-reset.check.cjs",
|
|
3065
|
+
"check:statusbar": "node scripts/status-bar.check.cjs",
|
|
3066
|
+
"check:dropfeedback": "node scripts/drop-feedback.check.cjs",
|
|
3067
|
+
"check:chatside": "node scripts/chat-sidepanel.check.cjs",
|
|
3068
|
+
"check:facade": "node scripts/facade-scope.check.cjs",
|
|
3069
|
+
"check:sessionmode": "node scripts/session-mode.check.cjs",
|
|
3070
|
+
"check:orbitside": "node scripts/orbitside.check.cjs",
|
|
3071
|
+
"check:sidewidth": "node scripts/side-width.check.cjs",
|
|
3072
|
+
"check:firstclick": "node scripts/first-click.check.cjs",
|
|
3073
|
+
"check:firstenter": "node scripts/first-enter.check.cjs",
|
|
3074
|
+
"fix:zhpunct": "node scripts/zh-punct.codemod.cjs",
|
|
3075
|
+
"shot:walk": "node scripts/uiux-walk.shot.cjs",
|
|
3076
|
+
"check:agentprofile": "node scripts/agent-profile.check.cjs",
|
|
3077
|
+
"check:projectdetails": "node scripts/project-details.check.cjs",
|
|
3078
|
+
"check:creationcard": "node scripts/creation-card.check.cjs",
|
|
3079
|
+
"check:teamoutputs": "node scripts/team-outputs.check.cjs",
|
|
3080
|
+
"check:teamprofile": "node scripts/team-profile.check.cjs",
|
|
3081
|
+
"check:teamdesk": "node scripts/team-desk.check.cjs",
|
|
3082
|
+
"check:ultra": "node scripts/ultra-effort.check.cjs",
|
|
3083
|
+
"check:canvas": "node scripts/e2e-editor.cjs --check=unified-canvas",
|
|
3084
|
+
"check:tutorial": "node scripts/e2e-editor.cjs --check=tutorial-note",
|
|
3085
|
+
"check:tabicon": "node scripts/e2e-editor.cjs --check=tab-icon",
|
|
3086
|
+
"shot:tabicon": "node scripts/e2e-editor.cjs --check=tab-icon --shot",
|
|
3087
|
+
"shot:tutorial": "node scripts/e2e-editor.cjs --check=tutorial-note --shot",
|
|
3088
|
+
"check:canvasreal": "node scripts/e2e-editor.cjs --check=canvas-realinput",
|
|
3089
|
+
"check:canvastouch": "node scripts/e2e-editor.cjs --check=canvas-touch",
|
|
3090
|
+
"check:blockops": "node scripts/e2e-editor.cjs --check=block-file-ops",
|
|
3091
|
+
"check:selection-display": "node scripts/e2e-editor.cjs --check=selection-display",
|
|
3092
|
+
"check:quickfind": "node scripts/e2e-editor.cjs --check=quickfind-cats",
|
|
3093
|
+
"check:findbar": "node scripts/findbar.check.cjs",
|
|
3094
|
+
"check:cssvar": "node scripts/css-var-override.check.cjs",
|
|
3095
|
+
"check:shadowcontract": "node scripts/shadow-contract.check.cjs",
|
|
3096
|
+
"check:shadowsurface": "node scripts/shadow-surface.check.cjs",
|
|
3097
|
+
"check:calendarui": "node scripts/calendar-ui.check.cjs",
|
|
3098
|
+
"check:astryxscope": "node scripts/astryx-scope.check.cjs",
|
|
3099
|
+
"check:todo": "node scripts/todo-view.check.cjs",
|
|
3100
|
+
"check:chattabs": "node scripts/chat-multitab.check.cjs",
|
|
3101
|
+
"check:newtab": "node scripts/newtab-open.check.cjs",
|
|
3102
|
+
"check:newnote": "node scripts/new-note-nav.check.cjs",
|
|
3103
|
+
"check:calendarplugin": "node scripts/builtin-calendar.check.cjs",
|
|
3104
|
+
"check:achtoast": "node scripts/ach-toast.check.cjs",
|
|
3105
|
+
"check:homepage": "node scripts/builtin-homepage.check.cjs",
|
|
3106
|
+
"check:artificial": "node scripts/builtin-artificial.check.cjs",
|
|
3107
|
+
"check:sandbox": "node scripts/sandbox.check.cjs",
|
|
3108
|
+
"check:homepageglass": "node scripts/homepage-glass.check.cjs",
|
|
3109
|
+
"check:homepagepinned": "node scripts/homepage-pinned-spaces.check.cjs",
|
|
3110
|
+
"check:homeslot": "node scripts/home-slot.check.cjs",
|
|
3111
|
+
"e2e:canvaslive": "node scripts/canvas-live.e2e.cjs",
|
|
3112
|
+
"e2e:attachment-clipboard": "node scripts/attachment-clipboard.e2e.cjs",
|
|
3113
|
+
"e2e:seams": "node scripts/plugin-seams.e2e.cjs",
|
|
3114
|
+
"check:imgsel": "node scripts/image-select.check.cjs",
|
|
3115
|
+
"check:coldrag": "node scripts/e2e-editor.cjs --check=columns-realdrag",
|
|
3116
|
+
"check:topdrag": "node scripts/e2e-editor.cjs --check=toplevel-realdrag",
|
|
3117
|
+
"check:dragdel": "node scripts/e2e-editor.cjs --check=unified-dragdelete",
|
|
3118
|
+
"check:formbase": "node scripts/form-baseline.check.cjs",
|
|
3119
|
+
"e2e:mediaembed": "node scripts/media-embed.e2e.cjs",
|
|
3120
|
+
"check:mediaembed": "node scripts/e2e-editor.cjs --check=media-embed",
|
|
3121
|
+
"check:notescroll": "node scripts/note-scroll-memory.check.cjs",
|
|
3122
|
+
"check:floatingtoc": "node scripts/floating-toc.check.cjs",
|
|
3123
|
+
"check:shellnoscroll": "node scripts/shell-noscroll.check.cjs",
|
|
3124
|
+
"check:manual": "node scripts/e2e-editor.cjs --check=manual-note",
|
|
3125
|
+
"shot:manual": "node scripts/e2e-editor.cjs --check=manual-note --shot",
|
|
3126
|
+
"e2e:coding": "node scripts/coding-studio.e2e.cjs",
|
|
3127
|
+
"e2e:image-studio": "node scripts/image-studio.e2e.cjs",
|
|
3128
|
+
"e2e:image-studio:live": "node scripts/image-studio.live.cjs",
|
|
3129
|
+
"check:permissions": "node scripts/permissions.check.cjs",
|
|
3130
|
+
"check:mininative": "node scripts/mini-auto.check.cjs --native-helper"
|
|
3131
|
+
},
|
|
3132
|
+
dependencies: {
|
|
3133
|
+
"@astryxdesign/core": "^0.1.4",
|
|
3134
|
+
"@astryxdesign/theme-neutral": "^0.1.4",
|
|
3135
|
+
"@aws-sdk/client-s3": "^3.1091.0",
|
|
3136
|
+
"@aws-sdk/lib-storage": "^3.1091.0",
|
|
3137
|
+
"@codemirror/language-data": "^6.5.1",
|
|
3138
|
+
"@dnd-kit/core": "^6.3.1",
|
|
3139
|
+
"@dnd-kit/sortable": "^10.0.0",
|
|
3140
|
+
"@dnd-kit/utilities": "^3.2.2",
|
|
3141
|
+
"@excalidraw/excalidraw": "npm:@zsviczian/excalidraw@0.18.122",
|
|
3142
|
+
"@forsion/extend": "0.7.1",
|
|
3143
|
+
"@forsion/tangu-computer-use": "0.6.1",
|
|
3144
|
+
"@milkdown/kit": "^7.21.2",
|
|
3145
|
+
"@milkdown/plugin-math": "^7.5.9",
|
|
3146
|
+
"@milkdown/react": "^7.21.2",
|
|
3147
|
+
"@milkdown/theme-nord": "^7.21.2",
|
|
3148
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
3149
|
+
"@stylexjs/stylex": "^0.18.3",
|
|
3150
|
+
"@uiw/react-codemirror": "^4.25.2",
|
|
3151
|
+
"@xterm/addon-fit": "^0.10.0",
|
|
3152
|
+
"@xterm/xterm": "^5.5.0",
|
|
3153
|
+
chokidar: "^5.0.0",
|
|
3154
|
+
diff: "^8.0.4",
|
|
3155
|
+
diff2html: "^3.4.55",
|
|
3156
|
+
dockview: "^7.0.2",
|
|
3157
|
+
"dockview-react": "^7.0.2",
|
|
3158
|
+
"docx-preview": "^0.3.5",
|
|
3159
|
+
"electron-updater": "^6.3.9",
|
|
3160
|
+
"framer-motion": "^12.40.0",
|
|
3161
|
+
jszip: "^3.10.1",
|
|
3162
|
+
katex: "^0.16.27",
|
|
3163
|
+
lowlight: "^3.3.0",
|
|
3164
|
+
"lucide-react": "^1.17.0",
|
|
3165
|
+
"lz-string": "^1.5.0",
|
|
3166
|
+
"micromark-util-classify-character": "^2.0.1",
|
|
3167
|
+
nanoid: "^5.1.11",
|
|
3168
|
+
"node-diff3": "^3.2.1",
|
|
3169
|
+
"pdf-lib": "^1.17.1",
|
|
3170
|
+
"pdfjs-dist": "^5.7.284",
|
|
3171
|
+
"perfect-freehand": "^1.2.3",
|
|
3172
|
+
qrcode: "^1.5.4",
|
|
3173
|
+
react: "^19.1.0",
|
|
3174
|
+
"react-dom": "^19.1.0",
|
|
3175
|
+
"react-markdown": "^10.1.0",
|
|
3176
|
+
"rehype-highlight": "^7.0.2",
|
|
3177
|
+
"rehype-katex": "^7.0.1",
|
|
3178
|
+
"remark-cjk-friendly": "^2.3.1",
|
|
3179
|
+
"remark-cjk-friendly-gfm-strikethrough": "^2.3.1",
|
|
3180
|
+
"remark-frontmatter": "^5.0.0",
|
|
3181
|
+
"remark-gfm": "^4.0.1",
|
|
3182
|
+
"remark-math": "^6.0.0",
|
|
3183
|
+
"remark-parse": "^11.0.0",
|
|
3184
|
+
"remark-stringify": "^11.0.0",
|
|
3185
|
+
"sherpa-onnx-node": "^1.13.4",
|
|
3186
|
+
sucrase: "^3.35.1",
|
|
3187
|
+
unified: "^11.0.5",
|
|
3188
|
+
"unist-util-visit": "^5.1.0",
|
|
3189
|
+
webdav: "^5.10.0",
|
|
3190
|
+
xlsx: "^0.18.5",
|
|
3191
|
+
zod: "^4.4.3",
|
|
3192
|
+
zustand: "^5.0.2"
|
|
3193
|
+
},
|
|
3194
|
+
devDependencies: {
|
|
3195
|
+
"@electron/rebuild": "^3.7.1",
|
|
3196
|
+
"@fontsource-variable/inter": "^5.3.0",
|
|
3197
|
+
"@fontsource-variable/jetbrains-mono": "^5.3.0",
|
|
3198
|
+
"@fontsource/lxgw-wenkai": "^5.3.0",
|
|
3199
|
+
"@fontsource/noto-sans-sc": "^5.3.0",
|
|
3200
|
+
"@iarna/toml": "^2.2.5",
|
|
3201
|
+
"@types/node": "^20.10.0",
|
|
3202
|
+
"@types/qrcode": "^1.5.6",
|
|
3203
|
+
"@types/react": "^19.0.0",
|
|
3204
|
+
"@types/react-dom": "^19.0.0",
|
|
3205
|
+
"@vitejs/plugin-react": "^5.1.4",
|
|
3206
|
+
electron: "^40.8.0",
|
|
3207
|
+
"electron-builder": "^26.15.3",
|
|
3208
|
+
"electron-vite": "^5.0.0",
|
|
3209
|
+
eslint: "^9.39.5",
|
|
3210
|
+
"eslint-plugin-react-hooks": "^5.2.0",
|
|
3211
|
+
"gray-matter": "^4.0.3",
|
|
3212
|
+
"happy-dom": "^20.10.6",
|
|
3213
|
+
"playwright-core": "^1.61.1",
|
|
3214
|
+
typescript: "^5.7.0",
|
|
3215
|
+
"typescript-eslint": "^8.65.0",
|
|
3216
|
+
vitest: "^3.0.0",
|
|
3217
|
+
yaml: "^2.9.0"
|
|
3218
|
+
},
|
|
3219
|
+
optionalDependencies: {
|
|
3220
|
+
"node-pty": "^1.1.0"
|
|
3221
|
+
}
|
|
3222
|
+
};
|
|
3223
|
+
|
|
3224
|
+
// unit/main.ts
|
|
3225
|
+
var dist = dirname4(fileURLToPath2(import.meta.url));
|
|
3226
|
+
var args = process.argv.slice(2);
|
|
3227
|
+
var commands = ["init", "install", "run", "migrate", "enable", "disable", "restart", "status", "access"];
|
|
3228
|
+
var action = commands.includes(args[0]) ? args.shift() : "run";
|
|
3229
|
+
var file = resolve7(args.shift() || (action === "init" ? "forsion-unit" : "unit.json"));
|
|
3230
|
+
var print = (result) => console.log(JSON.stringify(result, null, 2));
|
|
3231
|
+
async function findInstallation(config, id) {
|
|
3232
|
+
if (!id) throw new Error("A plugin id is required");
|
|
3233
|
+
for (const entry of installations(config)) if ((await readPackage(entry.path)).manifest.id === id) return entry;
|
|
3234
|
+
throw new Error(`Plugin is not installed: ${id}`);
|
|
3235
|
+
}
|
|
3236
|
+
async function run() {
|
|
3237
|
+
if (action === "init") {
|
|
3238
|
+
const option = (key, fallback) => {
|
|
3239
|
+
const i = args.indexOf(key);
|
|
3240
|
+
return i < 0 ? fallback : args[i + 1];
|
|
3241
|
+
};
|
|
3242
|
+
const mode = option("--mode", "public");
|
|
3243
|
+
if (!["public", "local"].includes(mode)) throw new Error("Mode must be public or local");
|
|
3244
|
+
const port = Number(option("--port", "3001")), basePath = option("--base", mode === "local" ? "/" : "/admin/");
|
|
3245
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535 || !basePath?.startsWith("/")) throw new Error("Invalid port or projection base path");
|
|
3246
|
+
await mkdir7(file, { recursive: true, mode: 448 });
|
|
3247
|
+
const path = resolve7(file, "unit.json");
|
|
3248
|
+
const config2 = {
|
|
3249
|
+
instanceId: randomUUID8(),
|
|
3250
|
+
name: "Forsion Unit",
|
|
3251
|
+
version: package_default.version,
|
|
3252
|
+
port,
|
|
3253
|
+
bindHost: option("--host", "127.0.0.1"),
|
|
3254
|
+
basePath,
|
|
3255
|
+
webDist: resolve7(dist, "web"),
|
|
3256
|
+
workerFile: resolve7(dist, "backendWorker.mjs"),
|
|
3257
|
+
dataDir: resolve7(file, "data"),
|
|
3258
|
+
plugins: [],
|
|
3259
|
+
...mode === "local" ? { workspace: { mode: "local" } } : {}
|
|
3260
|
+
};
|
|
3261
|
+
const handle = await open2(path, "wx", 384);
|
|
3262
|
+
try {
|
|
3263
|
+
await handle.writeFile(JSON.stringify(config2, null, 2) + "\n");
|
|
3264
|
+
} finally {
|
|
3265
|
+
await handle.close();
|
|
3266
|
+
}
|
|
3267
|
+
print({ config: path, instanceId: config2.instanceId });
|
|
3268
|
+
return;
|
|
3269
|
+
}
|
|
3270
|
+
if (action === "install") {
|
|
3271
|
+
if (!args[0]) throw new Error("A plugin package directory is required");
|
|
3272
|
+
print(await installPackage(file, resolve7(args[0])));
|
|
3273
|
+
return;
|
|
3274
|
+
}
|
|
3275
|
+
const config = await readConfig(file);
|
|
3276
|
+
config.workerFile ||= resolve7(dist, "backendWorker.mjs");
|
|
3277
|
+
if (action === "access") {
|
|
3278
|
+
if (config.workspace?.mode !== "local") throw new Error("Public sites use their Account provider");
|
|
3279
|
+
const token = await localOwnerToken(config.dataDir);
|
|
3280
|
+
print({ url: `http://${config.bindHost || "127.0.0.1"}:${config.port}${config.basePath}#unit-owner=${token}` });
|
|
3281
|
+
return;
|
|
3282
|
+
}
|
|
3283
|
+
if (action !== "run") {
|
|
3284
|
+
try {
|
|
3285
|
+
print(await sendControl(config.dataDir, { action, id: args[0] }));
|
|
3286
|
+
return;
|
|
3287
|
+
} catch (error) {
|
|
3288
|
+
if (!unitIsOffline(error)) throw error;
|
|
3289
|
+
}
|
|
3290
|
+
if (action === "status") {
|
|
3291
|
+
print({ running: false, instanceId: config.instanceId, plugins: await Promise.all(installations(config).map(async (p) => ({ id: (await readPackage(p.path)).manifest.id, enabled: p.enabled !== false }))) });
|
|
3292
|
+
return;
|
|
3293
|
+
}
|
|
3294
|
+
const entry = await findInstallation(config, args[0]);
|
|
3295
|
+
if (action === "enable" || action === "disable") {
|
|
3296
|
+
entry.enabled = action === "enable";
|
|
3297
|
+
await writeConfig(file, config);
|
|
3298
|
+
print({ id: args[0], enabled: entry.enabled });
|
|
3299
|
+
return;
|
|
3300
|
+
}
|
|
3301
|
+
if (action === "restart") throw new Error("Unit is not running");
|
|
3302
|
+
const pack = await readPackage(entry.path, config.version);
|
|
3303
|
+
if (!pack.backendEntry) throw new Error("Plugin has no backend migration");
|
|
3304
|
+
const dataDir = resolve7(config.dataDir, "plugins", pack.manifest.id);
|
|
3305
|
+
await mkdir7(dataDir, { recursive: true, mode: 448 });
|
|
3306
|
+
const migration = new AbortController();
|
|
3307
|
+
const cancelMigration = () => migration.abort(new Error("Migration interrupted"));
|
|
3308
|
+
for (const signal of ["SIGINT", "SIGTERM"]) process.on(signal, cancelMigration);
|
|
3309
|
+
try {
|
|
3310
|
+
await migrateBackend({
|
|
3311
|
+
signal: migration.signal,
|
|
3312
|
+
id: pack.manifest.id,
|
|
3313
|
+
entry: pack.backendEntry,
|
|
3314
|
+
packageDir: pack.root,
|
|
3315
|
+
dataDir,
|
|
3316
|
+
config: entry.config || {},
|
|
3317
|
+
env: { ...entry.config?.env || {}, ...entry.env },
|
|
3318
|
+
workerFile: config.workerFile,
|
|
3319
|
+
startTimeoutMs: 6e5
|
|
3320
|
+
});
|
|
3321
|
+
} finally {
|
|
3322
|
+
for (const signal of ["SIGINT", "SIGTERM"]) process.off(signal, cancelMigration);
|
|
3323
|
+
}
|
|
3324
|
+
print({ id: pack.manifest.id, migrated: true });
|
|
3325
|
+
return;
|
|
3326
|
+
}
|
|
3327
|
+
if (process.env.UNIT_PORT) config.port = Number(process.env.UNIT_PORT);
|
|
3328
|
+
await mkdir7(config.dataDir, { recursive: true, mode: 448 });
|
|
3329
|
+
const lockPath = resolve7(config.dataDir, "run.lock");
|
|
3330
|
+
let lock;
|
|
3331
|
+
try {
|
|
3332
|
+
lock = await open2(lockPath, "wx", 384);
|
|
3333
|
+
} catch (error) {
|
|
3334
|
+
if (error.code !== "EEXIST") throw error;
|
|
3335
|
+
const pid = Number(await readFile7(lockPath, "utf8"));
|
|
3336
|
+
if (!Number.isInteger(pid) || pid <= 0) throw new Error("Unit startup is locked; inspect data/run.lock");
|
|
3337
|
+
try {
|
|
3338
|
+
process.kill(pid, 0);
|
|
3339
|
+
throw new Error("This Unit is already running");
|
|
3340
|
+
} catch (failure) {
|
|
3341
|
+
if (failure.code !== "ESRCH") throw failure;
|
|
3342
|
+
}
|
|
3343
|
+
await rm4(lockPath);
|
|
3344
|
+
lock = await open2(lockPath, "wx", 384);
|
|
3345
|
+
}
|
|
3346
|
+
await lock.writeFile(String(process.pid));
|
|
3347
|
+
await lock.close();
|
|
3348
|
+
let unit;
|
|
3349
|
+
let control;
|
|
3350
|
+
let shutdownRequested = false, requestStop;
|
|
3351
|
+
const onSignal = () => {
|
|
3352
|
+
shutdownRequested = true;
|
|
3353
|
+
void requestStop?.();
|
|
3354
|
+
};
|
|
3355
|
+
for (const signal of ["SIGINT", "SIGTERM"]) process.on(signal, onSignal);
|
|
3356
|
+
try {
|
|
3357
|
+
unit = await startBasicUnit(config);
|
|
3358
|
+
let queue = Promise.resolve();
|
|
3359
|
+
const status = () => ({ running: true, pid: process.pid, instanceId: config.instanceId, port: unit.port, plugins: unit.status() });
|
|
3360
|
+
const dispatch = async (command) => {
|
|
3361
|
+
if (command.action === "status") return status();
|
|
3362
|
+
if (!["enable", "disable", "restart", "update", "migrate"].includes(command.action)) throw new Error("Unknown Unit command");
|
|
3363
|
+
const entry = await findInstallation(config, command.id);
|
|
3364
|
+
const previous = { ...entry };
|
|
3365
|
+
if (command.action === "update") {
|
|
3366
|
+
if (!command.path) throw new Error("An update package path is required");
|
|
3367
|
+
await unit.update(command.id, command.path);
|
|
3368
|
+
entry.path = resolve7(command.path);
|
|
3369
|
+
} else if (command.action === "enable") {
|
|
3370
|
+
await unit.enable(command.id);
|
|
3371
|
+
entry.enabled = true;
|
|
3372
|
+
} else if (command.action === "disable") {
|
|
3373
|
+
await unit.disable(command.id);
|
|
3374
|
+
entry.enabled = false;
|
|
3375
|
+
} else if (command.action === "restart") await unit.restart(command.id);
|
|
3376
|
+
else await unit.migrate(command.id);
|
|
3377
|
+
try {
|
|
3378
|
+
await writeConfig(file, config);
|
|
3379
|
+
} catch (error) {
|
|
3380
|
+
Object.assign(entry, previous);
|
|
3381
|
+
entry.enabled = previous.enabled;
|
|
3382
|
+
if (command.action === "update") await unit.update(command.id, previous.path);
|
|
3383
|
+
if (command.action === "enable" && previous.enabled === false) await unit.disable(command.id);
|
|
3384
|
+
if (command.action === "disable" && previous.enabled !== false) await unit.enable(command.id);
|
|
3385
|
+
throw error;
|
|
3386
|
+
}
|
|
3387
|
+
return status();
|
|
3388
|
+
};
|
|
3389
|
+
control = await startControl(config.dataDir, (command) => {
|
|
3390
|
+
const next = queue.then(() => dispatch(command));
|
|
3391
|
+
queue = next.then(() => {
|
|
3392
|
+
}, () => {
|
|
3393
|
+
});
|
|
3394
|
+
return next;
|
|
3395
|
+
});
|
|
3396
|
+
print(status());
|
|
3397
|
+
console.log(`[unit] ${config.name} http://${config.bindHost || "127.0.0.1"}:${unit.port}${config.basePath}`);
|
|
3398
|
+
let stopping = false;
|
|
3399
|
+
const stop = async () => {
|
|
3400
|
+
if (stopping) return;
|
|
3401
|
+
stopping = true;
|
|
3402
|
+
let failure = false;
|
|
3403
|
+
try {
|
|
3404
|
+
await control.close();
|
|
3405
|
+
await queue;
|
|
3406
|
+
await unit.close();
|
|
3407
|
+
} catch (error) {
|
|
3408
|
+
console.error(error);
|
|
3409
|
+
failure = true;
|
|
3410
|
+
} finally {
|
|
3411
|
+
await rm4(lockPath, { force: true });
|
|
3412
|
+
process.exit(failure ? 1 : 0);
|
|
3413
|
+
}
|
|
3414
|
+
};
|
|
3415
|
+
requestStop = stop;
|
|
3416
|
+
if (shutdownRequested) await stop();
|
|
3417
|
+
} catch (error) {
|
|
3418
|
+
for (const signal of ["SIGINT", "SIGTERM"]) process.off(signal, onSignal);
|
|
3419
|
+
try {
|
|
3420
|
+
await control?.close();
|
|
3421
|
+
} finally {
|
|
3422
|
+
try {
|
|
3423
|
+
await unit?.close();
|
|
3424
|
+
} finally {
|
|
3425
|
+
await rm4(lockPath, { force: true });
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3428
|
+
throw error;
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
await run().catch((error) => {
|
|
3432
|
+
console.error(`[unit] ${error instanceof Error ? error.message : "Command failed"}`);
|
|
3433
|
+
process.exitCode = 1;
|
|
3434
|
+
});
|