@runeya/runeya 2.0.39 → 2.0.41
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/agent-manager-2I47P7CA.js +7 -0
- package/{chunk-PW6A3BLD.js → chunk-NJJMESIY.js} +400 -93
- package/chunk-NJJMESIY.js.map +1 -0
- package/index.js +1 -1
- package/package.json +1 -1
- package/{src-A3WSWXEY.js → src-BCBD4MPB.js} +1041 -336
- package/src-BCBD4MPB.js.map +1 -0
- package/web/assets/{AgentScalarChatInterface.vue-HiYdPZh0.js → AgentScalarChatInterface.vue-bk9hxmaQ.js} +1 -1
- package/web/assets/{AgentTerminalPage-BV1OTr-r.js → AgentTerminalPage-B04ZDAX6.js} +1 -1
- package/web/assets/AiChatPage-CRVc_0Nu.js +1 -0
- package/web/assets/{AppLayout-DW_-KBWL.css → AppLayout-BBifPBJu.css} +1 -1
- package/web/assets/AppLayout-DvrAr1Tw.js +1 -0
- package/web/assets/{AuditPage-Bc4NZud3.js → AuditPage-B64guLBx.js} +1 -1
- package/web/assets/CloudAccountPage-4nEDwbbP.js +1 -0
- package/web/assets/CloudAccountPage-C_OlcjXc.js +1 -0
- package/web/assets/CloudAccountPage-ChVg2wnJ.css +1 -0
- package/web/assets/{CloudMessagingPage-CWcahAog.js → CloudMessagingPage-ClNaEqU1.js} +1 -1
- package/web/assets/{DashboardPage-nhKxGmnu.js → DashboardPage-8z5zI4At.js} +1 -1
- package/web/assets/{EditorIcon-vSDu502Z.js → EditorIcon-QuM8Jw0F.js} +1 -1
- package/web/assets/{EnvironmentForm-8WEcxUYY.js → EnvironmentForm-CctbC-gb.js} +1 -1
- package/web/assets/{EnvironmentFormPage-CpwzzSeB.js → EnvironmentFormPage-BOLckpdf.js} +1 -1
- package/web/assets/{EnvironmentListPage-Dyq7x-l_.js → EnvironmentListPage-CWbCtHUP.js} +1 -1
- package/web/assets/{EnvironmentSelector-BXe42bF9.js → EnvironmentSelector-CSSr9Uoa.js} +1 -1
- package/web/assets/{KanbanBoardPage-DHAWtkkJ.js → KanbanBoardPage-wjk3mufd.js} +3 -3
- package/web/assets/{KanbanListPage-CjQfqoXf.js → KanbanListPage-CBa8MOZq.js} +1 -1
- package/web/assets/LaunchProjectPage-CnhTqUn6.js +1 -0
- package/web/assets/LaunchProjectPage-CxXC2U3O.css +1 -0
- package/web/assets/{LaunchServicesPage-DYLGFHSm.js → LaunchServicesPage-oHDlEgqh.js} +1 -1
- package/web/assets/{LogViewer-CEQDlzBl.js → LogViewer-CbXki1mv.js} +1 -1
- package/web/assets/{ProcessControls-RuqRu4r_.js → ProcessControls-E3Ka8erw.js} +1 -1
- package/web/assets/{ProcessDetailPage-CnK8kLHx.js → ProcessDetailPage-lofKmzDr.js} +1 -1
- package/web/assets/{ProcessMetrics-tNC5ggO0.js → ProcessMetrics-DNIFOkvO.js} +1 -1
- package/web/assets/ProjectDetailPage-B42imx75.css +1 -0
- package/web/assets/ProjectDetailPage-Dz2WCS0H.js +1 -0
- package/web/assets/{ProjectListPage-BGihYeDx.js → ProjectListPage-MH2MfkaC.js} +1 -1
- package/web/assets/{RandomJobTestPage-BgjN2Lqt.js → RandomJobTestPage-C1yrM6Zy.js} +1 -1
- package/web/assets/{ScenarioEditorPage-nU_bK6Wm.js → ScenarioEditorPage-Dy6iy9r8.js} +1 -1
- package/web/assets/{SendToChatDialog-DG6GidKi.js → SendToChatDialog-DCpOcZ4B.js} +1 -1
- package/web/assets/{ServiceControlsBar-lvV0380I.js → ServiceControlsBar-B3dt-TpS.js} +1 -1
- package/web/assets/{ServiceDetailPage-gG_F4CpK.js → ServiceDetailPage-Dt4DRm6q.js} +1 -1
- package/web/assets/{ServiceFormPage-5pLarqaj.js → ServiceFormPage-ks_mGrTT.js} +1 -1
- package/web/assets/{SettingsPage-DGtNwgNF.js → SettingsPage-Ca--x3I2.js} +4 -4
- package/web/assets/{SetupPage-zLbZ7HFD.js → SetupPage-DnbrJAZf.js} +1 -1
- package/web/assets/{UnlockPage-BNUgOO2j.js → UnlockPage-Brv5TJMY.js} +1 -1
- package/web/assets/{VariablesTable-Dh7ySbKc.js → VariablesTable-DWg01MKw.js} +1 -1
- package/web/assets/{VersionBadge-Bcr43sQb.js → VersionBadge-DzEXeyD9.js} +1 -1
- package/web/assets/WikiPage-GjwXa8Oq.js +1 -0
- package/web/assets/{WikiView-bePXeeW1.js → WikiView-CulrPak1.js} +1 -1
- package/web/assets/{add-scalar-classes-BZ33Hgh4.js → add-scalar-classes-CqTKiXd8.js} +3 -3
- package/web/assets/auth-errors-CUm_h-nn.js +1 -0
- package/web/assets/{autocomplete-Mxh2ZegH.js → autocomplete-DEB2Qu-k.js} +1 -1
- package/web/assets/{basic-shape-diamond.svg-C_plmghk.js → basic-shape-diamond.svg-KT7ENzRk.js} +1 -1
- package/web/assets/{basic-shape-hexagon.svg-A1qvevt_.js → basic-shape-hexagon.svg-BG-V5Q5m.js} +1 -1
- package/web/assets/{basic-shape-primary-circle-ellipse-round.svg-Df8UWBuG.js → basic-shape-primary-circle-ellipse-round.svg-DkS-BSnd.js} +1 -1
- package/web/assets/{basic-shape-primary-square-rectangle.svg-hAiISQ_N.js → basic-shape-primary-square-rectangle.svg-JAOlu5yc.js} +1 -1
- package/web/assets/{basic-shape-shield.svg-j04XHSeq.js → basic-shape-shield.svg-BhPI20FC.js} +1 -1
- package/web/assets/{browser-_K3D-Q-7.js → browser-D77gc3Rr.js} +1 -1
- package/web/assets/{browser-B4MIWpa7.js → browser-Dh2HVE1I.js} +1 -1
- package/web/assets/{card-DGaeJPst.js → card-DIJopohC.js} +1 -1
- package/web/assets/{checkbox-C5uNb3Rm.js → checkbox-gEptCzM4.js} +1 -1
- package/web/assets/{chip-DDjgi9qj.js → chip-Rl8eKYOz.js} +1 -1
- package/web/assets/{client-D9DmTFwP.js → client-CPmn2crt.js} +1 -1
- package/web/assets/{cloud-chat.store-BPUP_Hxk.js → cloud-chat.store-DoQXE_CW.js} +1 -1
- package/web/assets/{cloud-session-B1oqwDJc.js → cloud-session-GnprlXNn.js} +1 -1
- package/web/assets/{column-sUzyGFk3.js → column-CHn9iHRy.js} +1 -1
- package/web/assets/{computer-device-desktop-monitor.svg-BBuhFhn-.js → computer-device-desktop-monitor.svg-DRH73_n8.js} +1 -1
- package/web/assets/{computer-device-desktop.svg-LL5HezrB.js → computer-device-desktop.svg-DXLn48TE.js} +1 -1
- package/web/assets/{computer-device-laptop.svg-C4xmfwdM.js → computer-device-laptop.svg-Cx3qvopX.js} +1 -1
- package/web/assets/{computer-device-mobile-phone-android-samsung-back.svg-B-uD42mr.js → computer-device-mobile-phone-android-samsung-back.svg-91VumZ5n.js} +1 -1
- package/web/assets/{computer-device-mobile-phone-android-samsung.svg-qPPbtnL9.js → computer-device-mobile-phone-android-samsung.svg-B1rGCVDW.js} +1 -1
- package/web/assets/{computer-device-mobile-phone-iphone-x-back.svg-B1Xa9B8p.js → computer-device-mobile-phone-iphone-x-back.svg-BpI2fov9.js} +1 -1
- package/web/assets/{computer-device-mobile-phone-iphone-x.svg-B9zWUZbf.js → computer-device-mobile-phone-iphone-x.svg-CgXARUwD.js} +1 -1
- package/web/assets/{computer-device-mobile-tablet-touch.svg-CriHRQuM.js → computer-device-mobile-tablet-touch.svg-CCbrFsKS.js} +1 -1
- package/web/assets/{computer-device-mobile-tablet.svg-BOKVzupN.js → computer-device-mobile-tablet.svg-DjMvb6nR.js} +1 -1
- package/web/assets/{computer-device-network-ethernet-cat6.svg-D0L6yx6b.js → computer-device-network-ethernet-cat6.svg-Bqaea3Cl.js} +1 -1
- package/web/assets/{computer-device-network-lan-www.svg-n9m6VZVC.js → computer-device-network-lan-www.svg-CJxUMWWI.js} +1 -1
- package/web/assets/{computer-device-network-wifi-connection.svg-BqJzn-5W.js → computer-device-network-wifi-connection.svg-BUWGehfn.js} +1 -1
- package/web/assets/{computer-device-network-wifi-router.svg-DKcOeajJ.js → computer-device-network-wifi-router.svg-BC12dD1O.js} +1 -1
- package/web/assets/{cssMode-CLuhsa-I.js → cssMode-Df4SqVsZ.js} +1 -1
- package/web/assets/{divider-UPIM4dQl.js → divider-xI7dNZxb.js} +1 -1
- package/web/assets/{ecology-science-erlenmeyer-flask.svg-DyL2i1-k.js → ecology-science-erlenmeyer-flask.svg-DsEq5QkC.js} +1 -1
- package/web/assets/{fieldset-CIZfn4mb.js → fieldset-DHi1E3Rv.js} +1 -1
- package/web/assets/{freemarker2-BQV9kA1Y.js → freemarker2-DcSf8AuF.js} +1 -1
- package/web/assets/{handlebars-ib1US5Pw.js → handlebars-B866hvQq.js} +1 -1
- package/web/assets/{html-D-hYOiBO.js → html-Bqs2Jvnl.js} +1 -1
- package/web/assets/{htmlMode-WY_1KZ8Q.js → htmlMode-ZcQnxlbi.js} +1 -1
- package/web/assets/{image-flash-lightning.svg-wPJhlaEM.js → image-flash-lightning.svg-BnJa70PB.js} +1 -1
- package/web/assets/{image-picture-flower.svg-C15wjNsW.js → image-picture-flower.svg-CnHedFRr.js} +1 -1
- package/web/assets/{index-CcDpHMIW.js → index-DKBnMXVN.js} +259 -259
- package/web/assets/{interface-alert-exclamation-diamond.svg-C2MhzWE9.js → interface-alert-exclamation-diamond.svg-COmucSk_.js} +1 -1
- package/web/assets/{interface-alert-exclamation-triangle-warning.svg-D8heEy48.js → interface-alert-exclamation-triangle-warning.svg-M36BIAo6.js} +1 -1
- package/web/assets/{interface-alert-information-circle.svg-zrf3-9VP.js → interface-alert-information-circle.svg-1kMmqhO2.js} +1 -1
- package/web/assets/{interface-award-crown.svg-CT401HIW.js → interface-award-crown.svg-DptGWkkU.js} +1 -1
- package/web/assets/{interface-bookmark.svg-CqHV5l4k.js → interface-bookmark-tag.svg-D7TAAj3n.js} +1 -1
- package/web/assets/{interface-bookmark-tag.svg-h5CZEHt8.js → interface-bookmark.svg-ViiyYT_L.js} +1 -1
- package/web/assets/{interface-calendar-date-one.svg-DUdLsM_8.js → interface-calendar-date-one.svg-CWorrxsZ.js} +1 -1
- package/web/assets/{interface-content-book-open-pages.svg-DNR8SCd0.js → interface-content-book-open-pages.svg-Br_e_4Im.js} +1 -1
- package/web/assets/{interface-content-book-page.svg-Da9e-SX6.js → interface-content-book-page.svg-Du0uHoZ7.js} +1 -1
- package/web/assets/{interface-content-file.svg-BZM7jMWF.js → interface-content-file.svg-C972356w.js} +1 -1
- package/web/assets/{interface-content-folder.svg-BisjJGhi.js → interface-content-folder.svg-Erjx4HGU.js} +1 -1
- package/web/assets/interface-content-folder.svg-nZraYlQ1.js +1 -0
- package/web/assets/{interface-copy-clipboard.svg-AIcyJXPF.js → interface-copy-clipboard.svg-B9XvjK6U.js} +1 -1
- package/web/assets/{interface-edit-attachment.svg-XSvdzubS.js → interface-edit-attachment.svg-BWe41qK5.js} +1 -1
- package/web/assets/{interface-edit-binocular.svg-CCB9rIw_.js → interface-edit-binocular.svg-ChJsM_P7.js} +1 -1
- package/web/assets/{interface-edit-magic-wand.svg-Dd4afdpz.js → interface-edit-magic-wand.svg-BUs2NS7s.js} +1 -1
- package/web/assets/{interface-edit-tool-paint-roller.svg-DpO7rr7P.js → interface-edit-tool-paint-roller.svg-CdrYKzH6.js} +1 -1
- package/web/assets/{interface-edit-tool-pencil.svg-DbKruCrP.js → interface-edit-tool-pencil.svg-C8on9j4c.js} +1 -1
- package/web/assets/{interface-favorite-award.svg-DNIpxtuy.js → interface-favorite-award.svg-Bh_lIFBs.js} +1 -1
- package/web/assets/{interface-favorite-flag.svg-CgGWpm8Y.js → interface-favorite-flag.svg-BfecybVt.js} +1 -1
- package/web/assets/{interface-favorite-heart.svg-Bke1OXMv.js → interface-favorite-heart.svg-DDreuf_y.js} +1 -1
- package/web/assets/{interface-favorite-star.svg-DgwsiTjP.js → interface-favorite-star.svg-BJNTsPaT.js} +1 -1
- package/web/assets/{interface-favorite-stars-sparkles.svg-dKGlP2sr.js → interface-favorite-stars-sparkles.svg-CKOBWT4J.js} +1 -1
- package/web/assets/{interface-hierarchy-flowchart.svg-Bved245R.js → interface-hierarchy-flowchart.svg-7rufYb_n.js} +1 -1
- package/web/assets/{interface-home-house.svg-DJIEYhIa.js → interface-home-house.svg-CrKToh2q.js} +1 -1
- package/web/assets/{interface-hyperlink.svg-BBh6NFX7.js → interface-hyperlink.svg-BOjFniK_.js} +1 -1
- package/web/assets/{interface-lighting-brightness.svg-NqCt5nK8.js → interface-lighting-brightness.svg-B3-sdI-a.js} +1 -1
- package/web/assets/{interface-lock-closed.svg-CclDxo01.js → interface-lock-closed.svg-D3pw1rIY.js} +1 -1
- package/web/assets/{interface-lock-open-unlock.svg-CYPJVug8.js → interface-lock-open-unlock.svg-CJ_XAmbt.js} +1 -1
- package/web/assets/{interface-login-key.svg-DaW7pEY5.js → interface-login-key.svg-D--OEw7I.js} +1 -1
- package/web/assets/{interface-search.svg-j89Vz_N5.js → interface-search.svg-_5NliDrL.js} +1 -1
- package/web/assets/{interface-setting-cog.svg-CY2WkccU.js → interface-setting-cog.svg-uK8uMEZ2.js} +1 -1
- package/web/assets/{interface-share-megaphone-bullhorn.svg-C927tb6q.js → interface-share-megaphone-bullhorn.svg-CIdM6tKR.js} +1 -1
- package/web/assets/{interface-share-rocket.svg-B3sZvIoQ.js → interface-share-rocket.svg-CN-zhWTK.js} +1 -1
- package/web/assets/{interface-share-satellite.svg-CQ8A4dXG.js → interface-share-satellite.svg-CVTJ9m_O.js} +1 -1
- package/web/assets/{interface-share-space-ship.svg-BTGyj986.js → interface-share-space-ship.svg-jIDLHLi6.js} +1 -1
- package/web/assets/{interface-share.svg-BSQ8cEXV.js → interface-share.svg-33frw3Wv.js} +1 -1
- package/web/assets/{interface-signal-square.svg-1h2-Znr2.js → interface-signal-square.svg-Wg7BKl9x.js} +1 -1
- package/web/assets/{interface-time-clock-circle.svg-CgUcH4er.js → interface-time-clock-circle.svg-CZ6md3Ju.js} +1 -1
- package/web/assets/{interface-time-hour-glass.svg-Biz9ylcj.js → interface-time-hour-glass.svg-dRVhEG12.js} +1 -1
- package/web/assets/{interface-users-multiple.svg-B2SNNyd6.js → interface-users-multiple.svg-D7jrCQNN.js} +1 -1
- package/web/assets/{interface-weather-moon.svg-DFIwJsKU.js → interface-weather-moon.svg-BYWPWUQx.js} +1 -1
- package/web/assets/{javascript-w0UZtcmg.js → javascript-BATb-HYh.js} +1 -1
- package/web/assets/{jsonMode-CJUdXIyV.js → jsonMode-CP6RXg9k.js} +1 -1
- package/web/assets/{liquid-CjedKIbd.js → liquid-BCpH2Ett.js} +1 -1
- package/web/assets/{lspLanguageFeatures-DGvsogF_.js → lspLanguageFeatures-BWSdWSw7.js} +1 -1
- package/web/assets/{mail-chat-bubble-square.svg-D16l2agS.js → mail-chat-bubble-square.svg-BwFA9sJS.js} +1 -1
- package/web/assets/{mail-send-email-paper-airplane.svg-Bz-my4zw.js → mail-send-email-paper-airplane.svg-DWRvHccC.js} +1 -1
- package/web/assets/{mail-send-envelope.svg-Cgh2a1hO.js → mail-send-envelope.svg-6QBx4SLE.js} +1 -1
- package/web/assets/{mdx-Y5OqFrRV.js → mdx-JagJo-dt.js} +1 -1
- package/web/assets/{message-Bh-eAVAb.js → message-TwI_r5mZ.js} +1 -1
- package/web/assets/{minus-CxgjeEyu.js → minus-oYqzuR4a.js} +1 -1
- package/web/assets/modal-BEYg-JLN.js +1 -0
- package/web/assets/{modal-ciXJUOol.js → modal-CFM8h6A5.js} +1 -1
- package/web/assets/{money-cashier-receipt.svg-BP5bjawX.js → money-cashier-receipt.svg-Cl2LyS7q.js} +1 -1
- package/web/assets/{money-currency-dollar-pay.svg-vKtIhvs8.js → money-currency-dollar-pay.svg-DiytkCh0.js} +1 -1
- package/web/assets/{money-graph-arrow-increase.svg-CaeoOB93.js → money-graph-arrow-increase.svg-D4qMSRlp.js} +1 -1
- package/web/assets/{money-graph-bar-chart-increase.svg-CjRjHwbF.js → money-graph-bar-chart-increase.svg-CdCIWvvQ.js} +1 -1
- package/web/assets/{multiselect-CsE4Xly1.js → multiselect-CSyl93Ho.js} +1 -1
- package/web/assets/{nature-ecology-leaf.svg-umy4cUUw.js → nature-ecology-leaf.svg-GjBsj54D.js} +1 -1
- package/web/assets/{password-Brra1Twl.js → password-xomJK8iH.js} +1 -1
- package/web/assets/{phone-telephone.svg-DQ18gSw7.js → phone-telephone.svg-CBtGAjHv.js} +1 -1
- package/web/assets/{plus-e_D9UcW9.js → plus-C8MR9x_j.js} +1 -1
- package/web/assets/process.store-vy8KqH_z.js +1 -0
- package/web/assets/{programming-bug.svg-BL4Hus_Q.js → programming-bug.svg-BHCuUnau.js} +1 -1
- package/web/assets/{programming-cloud.svg-DrTlrDBe.js → programming-cloud.svg-DPdcvBlm.js} +1 -1
- package/web/assets/{programming-computer-database-server.svg-CnwbhD5W.js → programming-computer-database-server.svg-CaruocqU.js} +1 -1
- package/web/assets/{programming-computer-database.svg-GymDSTl9.js → programming-computer-database.svg-DFSRGAGE.js} +1 -1
- package/web/assets/{programming-module-four-layout.svg-DspyCTiu.js → programming-module-four-layout.svg-GUN-7Tqn.js} +1 -1
- package/web/assets/{programming-module-three.svg-BuYXTrWu.js → programming-module-three.svg-DYC-KU3a.js} +1 -1
- package/web/assets/{programming-module.svg-DIlRMVbW.js → programming-module.svg-NW_oL81i.js} +1 -1
- package/web/assets/{programming-script-code.svg-DPXJcL07.js → programming-script-code.svg-BPISkQtk.js} +1 -1
- package/web/assets/{python-3fAbpxqq.js → python-CXVm4gDt.js} +1 -1
- package/web/assets/{radiobutton-w7t3rGDJ.js → radiobutton-BLlyiAho.js} +1 -1
- package/web/assets/{razor-GDM0jSNH.js → razor-BcnFJswZ.js} +1 -1
- package/web/assets/router-Z6Tmw2JR.js +1 -0
- package/web/assets/{settings.store-D0eLKI4A.js → settings.store-CPAOxEyN.js} +1 -1
- package/web/assets/{shopping-cart.svg-B1y-4w0M.js → shopping-cart.svg-CA8D1Bj1.js} +1 -1
- package/web/assets/{shopping-gift-present.svg-DJ3mGWql.js → shopping-gift-present.svg-DA0s4ges.js} +1 -1
- package/web/assets/{shopping-shipping-box-parcel-package.svg-Bj3iHRT5.js → shopping-shipping-box-parcel-package.svg-BkLM7KC9.js} +1 -1
- package/web/assets/{src-wVYcxep_.js → src-DQG3PBVG.js} +1 -1
- package/web/assets/{src-BotO3dQ7.js → src-Dyv5MRfg.js} +1 -1
- package/web/assets/{tabpanel-DOZdp_HS.js → tabpanel-CNQhqmGV.js} +1 -1
- package/web/assets/{tag-new-circle.svg-Bb_ByVey.js → tag-new-circle.svg-BVZMrQWv.js} +1 -1
- package/web/assets/{travel-map-earth-globe.svg-Mso-K6XY.js → travel-map-earth-globe.svg-BZXWAlAZ.js} +1 -1
- package/web/assets/trpc-DVqzpQq9.js +1 -0
- package/web/assets/{tsMode-BUHQk9ZI.js → tsMode-BjuYVSR3.js} +1 -1
- package/web/assets/{typescript-Quc5d9qP.js → typescript-Bw5AHC7f.js} +1 -1
- package/web/assets/useAiChatDrawer-BCH3idQo.js +1 -0
- package/web/assets/useCloudOrg-A1wW_KrI.css +1 -0
- package/web/assets/useCloudOrg-BthYAZho.js +1 -0
- package/web/assets/{xml-CkYBw41O.js → xml-ESsLInqI.js} +1 -1
- package/web/assets/{yaml-CmSvkxzN.js → yaml-CEuOVTz-.js} +1 -1
- package/web/index.html +1 -1
- package/agent-manager-U3HLZ6QZ.js +0 -7
- package/chunk-PW6A3BLD.js.map +0 -1
- package/src-A3WSWXEY.js.map +0 -1
- package/web/assets/AiChatPage-Ct_f2ned.js +0 -1
- package/web/assets/AppLayout-BX_Buqtz.js +0 -1
- package/web/assets/CloudAccountPage-BqnCKZxn.js +0 -1
- package/web/assets/CloudAccountPage-CRN-tnI1.css +0 -1
- package/web/assets/CloudAccountPage-kTdYEt2S.js +0 -1
- package/web/assets/LaunchProjectPage-57kJRakH.css +0 -1
- package/web/assets/LaunchProjectPage-Bfs_AIgu.js +0 -1
- package/web/assets/ProjectDetailPage-BxLE6-54.css +0 -1
- package/web/assets/ProjectDetailPage-Cd3-8PAI.js +0 -1
- package/web/assets/WikiPage-DDx_jeaY.js +0 -1
- package/web/assets/auth-errors-BJV46AcQ.js +0 -1
- package/web/assets/interface-content-folder.svg-p32kmmD9.js +0 -1
- package/web/assets/modal-CIFHhhV7.js +0 -1
- package/web/assets/process.store-BhNwQ2dn.js +0 -1
- package/web/assets/router-C6yOFC1g.js +0 -1
- package/web/assets/trpc-BWtKBok1.js +0 -1
- package/web/assets/useAiChatDrawer-CRJCTTTO.js +0 -1
- package/web/assets/useCloudOrg-CBr3kqWH.js +0 -1
- package/web/assets/useCloudOrg-Gz6ek71z.css +0 -1
- /package/{agent-manager-U3HLZ6QZ.js.map → agent-manager-2I47P7CA.js.map} +0 -0
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
agentManager,
|
|
32
32
|
agentSpawner,
|
|
33
33
|
agentStore,
|
|
34
|
+
allVaultRoots,
|
|
34
35
|
clearEncryptionKey,
|
|
35
36
|
clearPendingLegacyKey,
|
|
36
37
|
decryptValue,
|
|
@@ -38,9 +39,11 @@ import {
|
|
|
38
39
|
encryptValue,
|
|
39
40
|
encryptVariableSlots,
|
|
40
41
|
ensureEncryptionKey,
|
|
42
|
+
ensureLocalVault,
|
|
41
43
|
env,
|
|
42
44
|
environmentStore,
|
|
43
45
|
extractEncryptedVariableSlots,
|
|
46
|
+
forgetProject,
|
|
44
47
|
getCollection,
|
|
45
48
|
getEncryptionKey,
|
|
46
49
|
getPendingLegacyKey,
|
|
@@ -48,17 +51,31 @@ import {
|
|
|
48
51
|
hasPendingLegacyKey,
|
|
49
52
|
isCiMode,
|
|
50
53
|
isEncryptedValue,
|
|
54
|
+
keyRoot,
|
|
55
|
+
keyedFilePath,
|
|
56
|
+
legacyMachineFilePath,
|
|
57
|
+
listCloudVaults,
|
|
51
58
|
loadFromEnv,
|
|
59
|
+
localVaultRoot,
|
|
60
|
+
machineFilePath,
|
|
61
|
+
machineRoot,
|
|
52
62
|
nativeParsers,
|
|
63
|
+
orgVaultRoot,
|
|
64
|
+
projectVaultRoot,
|
|
53
65
|
readEntity,
|
|
66
|
+
registerProjectLoader,
|
|
67
|
+
rootOfProject,
|
|
54
68
|
serviceStore,
|
|
55
69
|
setEncryptionKey,
|
|
56
70
|
setEncryptionKeyInMemory,
|
|
57
71
|
setPendingLegacyKey,
|
|
72
|
+
setProjectServices,
|
|
73
|
+
setProjectVault,
|
|
58
74
|
stableEncryptValue,
|
|
59
75
|
traefikManager,
|
|
76
|
+
vaultFor,
|
|
60
77
|
writeEntity
|
|
61
|
-
} from "./chunk-
|
|
78
|
+
} from "./chunk-NJJMESIY.js";
|
|
62
79
|
|
|
63
80
|
// ../server/src/create-server.ts
|
|
64
81
|
import express from "express";
|
|
@@ -101,7 +118,7 @@ async function verifyJwt(token) {
|
|
|
101
118
|
// ../server/src/services/api-key-store.ts
|
|
102
119
|
import { randomBytes as randomBytes2, randomUUID, timingSafeEqual } from "crypto";
|
|
103
120
|
import { readFile, writeFile, rename, mkdir, chmod } from "fs/promises";
|
|
104
|
-
import {
|
|
121
|
+
import { dirname } from "path";
|
|
105
122
|
function maskApiKey(apiKey) {
|
|
106
123
|
const { key, ...rest } = apiKey;
|
|
107
124
|
return { ...rest, keyPreview: key.slice(-4) };
|
|
@@ -111,15 +128,19 @@ var FILE_PRIVATE = "api-keys.private.json";
|
|
|
111
128
|
var ApiKeyStore = class {
|
|
112
129
|
keys = /* @__PURE__ */ new Map();
|
|
113
130
|
loaded = false;
|
|
131
|
+
/**
|
|
132
|
+
* Encrypted at rest, so the file sits beside the key that opens it — read
|
|
133
|
+
* under a different key it would decrypt to blanks, then be saved back blank.
|
|
134
|
+
*/
|
|
114
135
|
filePath(isPublic) {
|
|
115
|
-
return
|
|
136
|
+
return keyedFilePath(isPublic ? FILE_PUBLIC : FILE_PRIVATE);
|
|
116
137
|
}
|
|
117
138
|
async ensureDir() {
|
|
118
|
-
await mkdir(
|
|
139
|
+
await mkdir(dirname(await this.filePath(true)), { recursive: true });
|
|
119
140
|
}
|
|
120
141
|
async persistFile(isPublic) {
|
|
121
142
|
await this.ensureDir();
|
|
122
|
-
const path = this.filePath(isPublic);
|
|
143
|
+
const path = await this.filePath(isPublic);
|
|
123
144
|
const keys = Array.from(this.keys.values()).filter((k) => k.isPublic === isPublic).map((k) => ({ ...k, key: encryptValue(k.key) }));
|
|
124
145
|
const tmp = path + ".tmp";
|
|
125
146
|
await writeFile(tmp, JSON.stringify({ keys }, null, 2), "utf-8");
|
|
@@ -145,8 +166,8 @@ var ApiKeyStore = class {
|
|
|
145
166
|
async load() {
|
|
146
167
|
if (this.loaded) return;
|
|
147
168
|
this.loaded = true;
|
|
148
|
-
await this.loadFile(this.filePath(true));
|
|
149
|
-
await this.loadFile(this.filePath(false));
|
|
169
|
+
await this.loadFile(await this.filePath(true));
|
|
170
|
+
await this.loadFile(await this.filePath(false));
|
|
150
171
|
}
|
|
151
172
|
/**
|
|
152
173
|
* Keys as exposed to callers — secrets stripped. Use `reveal` to obtain one
|
|
@@ -419,53 +440,201 @@ import { TRPCError as TRPCError2 } from "@trpc/server";
|
|
|
419
440
|
// ../server/src/services/project-store.ts
|
|
420
441
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
421
442
|
import { EventEmitter } from "events";
|
|
422
|
-
import { readFile as readFile2, writeFile as
|
|
443
|
+
import { readFile as readFile2, writeFile as writeFile3, rename as rename2, mkdir as mkdir2, chmod as chmod2, rm } from "fs/promises";
|
|
423
444
|
import { join as join2 } from "path";
|
|
424
|
-
|
|
445
|
+
|
|
446
|
+
// ../server/src/utils/ensure-data-gitignore.ts
|
|
447
|
+
import { writeFile as writeFile2 } from "fs/promises";
|
|
448
|
+
import { join } from "path";
|
|
449
|
+
var GITIGNORE_CONTENT = `
|
|
450
|
+
#################
|
|
451
|
+
# Do not edit this file #
|
|
452
|
+
#################
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
# Encryption key (contains secrets \u2014 never commit)
|
|
456
|
+
.encryption.key
|
|
457
|
+
.encryption.key.enc
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
# Key verification canary \u2014 MUST be committed so new team members can validate their encryption key
|
|
461
|
+
!.runeya-verify
|
|
462
|
+
|
|
463
|
+
# This machine's cloud session token (encrypted, and specific to this install)
|
|
464
|
+
cloud-session.json
|
|
465
|
+
|
|
466
|
+
# Backup in local only
|
|
467
|
+
backup-*
|
|
468
|
+
|
|
469
|
+
# Per-user environment overrides (never commit \u2014 each dev has their own)
|
|
470
|
+
environment-overrides.json
|
|
471
|
+
|
|
472
|
+
# Private user data (conversations, personal state \u2014 never commit)
|
|
473
|
+
*.private
|
|
474
|
+
*.private.json*
|
|
475
|
+
conversations/
|
|
476
|
+
conversations.private.json
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
# Runtime logs
|
|
481
|
+
*.log
|
|
482
|
+
*.log.*
|
|
483
|
+
`;
|
|
484
|
+
async function ensureDataGitignore(dataDir) {
|
|
485
|
+
const gitignorePath = join(dataDir, ".gitignore");
|
|
486
|
+
await writeFile2(gitignorePath, GITIGNORE_CONTENT, "utf-8");
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// ../server/src/services/project-store.ts
|
|
490
|
+
var FILENAME = "project.json";
|
|
491
|
+
var LEGACY_FILENAME = "projects.json";
|
|
425
492
|
function toDiskForm(project) {
|
|
426
493
|
const { activeEnvironmentId: _runtimeOnly, ...rest } = project;
|
|
427
494
|
return rest;
|
|
428
495
|
}
|
|
496
|
+
async function readLocalProject() {
|
|
497
|
+
const root = localVaultRoot();
|
|
498
|
+
let current = null;
|
|
499
|
+
try {
|
|
500
|
+
const raw = await readFile2(join2(root, FILENAME), "utf-8");
|
|
501
|
+
const parsed = ProjectSchema.safeParse(JSON.parse(raw));
|
|
502
|
+
if (parsed.success) current = parsed.data;
|
|
503
|
+
} catch {
|
|
504
|
+
}
|
|
505
|
+
let legacy;
|
|
506
|
+
try {
|
|
507
|
+
legacy = JSON.parse(await readFile2(join2(root, LEGACY_FILENAME), "utf-8"));
|
|
508
|
+
} catch {
|
|
509
|
+
return { local: current, strayCloud: [] };
|
|
510
|
+
}
|
|
511
|
+
const strayCloud = legacy.filter((p) => p.orgId);
|
|
512
|
+
if (current) return { local: current, strayCloud };
|
|
513
|
+
const locals = legacy.filter((p) => !p.orgId);
|
|
514
|
+
if (locals.length > 1) {
|
|
515
|
+
const names = locals.map((p) => `"${p.name}" (${p.id})`).join(", ");
|
|
516
|
+
throw new Error(
|
|
517
|
+
`${join2(root, LEGACY_FILENAME)} holds ${locals.length} local projects: ${names}. A directory now holds a single local project \u2014 move the others to their own directory, or into an organization, then start again.`
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
return { local: locals[0] ?? null, strayCloud };
|
|
521
|
+
}
|
|
522
|
+
async function readCloudProjects() {
|
|
523
|
+
const found = [];
|
|
524
|
+
for (const { orgId, projectId, root } of await listCloudVaults()) {
|
|
525
|
+
try {
|
|
526
|
+
const raw = await readFile2(join2(root, FILENAME), "utf-8");
|
|
527
|
+
const parsed = ProjectSchema.safeParse({ ...JSON.parse(raw), id: projectId, orgId });
|
|
528
|
+
if (parsed.success) found.push({ project: parsed.data, root });
|
|
529
|
+
} catch {
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return found;
|
|
533
|
+
}
|
|
534
|
+
var LocalProjectExistsError = class extends Error {
|
|
535
|
+
constructor(projectName) {
|
|
536
|
+
super(
|
|
537
|
+
`This directory already holds the local project "${projectName}". A directory holds one local project: use another directory, or create the project in an organization.`
|
|
538
|
+
);
|
|
539
|
+
this.projectName = projectName;
|
|
540
|
+
this.name = "LocalProjectExistsError";
|
|
541
|
+
}
|
|
542
|
+
code = "LOCAL_PROJECT_EXISTS";
|
|
543
|
+
};
|
|
429
544
|
var ProjectStore = class {
|
|
430
545
|
projects = /* @__PURE__ */ new Map();
|
|
431
546
|
loaded = false;
|
|
432
547
|
/** Emits `'saved'` after every persisted mutation, for the cloud sync bridge. */
|
|
433
548
|
events = new EventEmitter();
|
|
434
|
-
getFilePath() {
|
|
435
|
-
return join2(
|
|
549
|
+
getFilePath(root = localVaultRoot()) {
|
|
550
|
+
return join2(root, FILENAME);
|
|
436
551
|
}
|
|
437
|
-
async
|
|
438
|
-
|
|
552
|
+
async writeAtomic(filePath, data) {
|
|
553
|
+
const tmpPath = filePath + ".tmp";
|
|
554
|
+
await writeFile3(tmpPath, data, "utf-8");
|
|
555
|
+
await rename2(tmpPath, filePath);
|
|
556
|
+
await chmod2(filePath, 384);
|
|
557
|
+
}
|
|
558
|
+
/** Adopt a project read from disk, remembering which vault it came from. */
|
|
559
|
+
async absorb(p, root) {
|
|
560
|
+
const projectEnvs = await environmentStore.getByProjectId(p.id).catch(() => []);
|
|
561
|
+
const defaultEnv = projectEnvs.find((e) => e.default);
|
|
562
|
+
this.projects.set(p.id, {
|
|
563
|
+
...p,
|
|
564
|
+
environmentIds: p.environmentIds ?? [],
|
|
565
|
+
groupOrder: p.groupOrder ?? [],
|
|
566
|
+
// Prefer the env marked default, fall back to first env
|
|
567
|
+
activeEnvironmentId: defaultEnv?.id ?? p.environmentIds?.[0]
|
|
568
|
+
});
|
|
569
|
+
setProjectVault(p.id, root);
|
|
570
|
+
setProjectServices(p.id, p.serviceIds ?? []);
|
|
439
571
|
}
|
|
572
|
+
/**
|
|
573
|
+
* Read every vault: the launch directory, then each organization's projects.
|
|
574
|
+
*
|
|
575
|
+
* Cloud vaults are discovered by walking `~/.runeya/orgs/*/projects/*` rather
|
|
576
|
+
* than from an index. An index is one more thing that can disagree with the
|
|
577
|
+
* disk — and a project present on disk but missing from it is exactly the
|
|
578
|
+
* "already here, still shown as downloadable" bug.
|
|
579
|
+
*/
|
|
440
580
|
async load() {
|
|
441
581
|
if (this.loaded) return;
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
this.
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
groupOrder: p.groupOrder ?? [],
|
|
452
|
-
// Prefer the env marked default, fall back to first env
|
|
453
|
-
activeEnvironmentId: defaultEnv?.id ?? p.environmentIds?.[0]
|
|
454
|
-
});
|
|
582
|
+
this.loaded = true;
|
|
583
|
+
const { local, strayCloud } = await readLocalProject();
|
|
584
|
+
if (local) await this.absorb(local, localVaultRoot());
|
|
585
|
+
let migrated = false;
|
|
586
|
+
for (const project of strayCloud) {
|
|
587
|
+
try {
|
|
588
|
+
await this.absorb(project, vaultFor(project).root);
|
|
589
|
+
migrated = true;
|
|
590
|
+
} catch {
|
|
455
591
|
}
|
|
456
|
-
} catch {
|
|
457
592
|
}
|
|
458
|
-
|
|
593
|
+
for (const { project, root } of await readCloudProjects()) {
|
|
594
|
+
await this.absorb(project, root);
|
|
595
|
+
}
|
|
596
|
+
if (migrated) {
|
|
597
|
+
await this.save();
|
|
598
|
+
await serviceStore.relocateAll();
|
|
599
|
+
await environmentStore.relocateAll();
|
|
600
|
+
}
|
|
459
601
|
}
|
|
602
|
+
/**
|
|
603
|
+
* Persist every vault this store knows about.
|
|
604
|
+
*
|
|
605
|
+
* Writing all of them rather than only the one that changed keeps a project
|
|
606
|
+
* that moved between vaults — a local project adopted by an org, a cloud
|
|
607
|
+
* project pulled for the first time — from lingering in the vault it left.
|
|
608
|
+
*/
|
|
460
609
|
async save() {
|
|
461
|
-
|
|
462
|
-
const
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
610
|
+
const locals = [];
|
|
611
|
+
const cloud = [];
|
|
612
|
+
for (const project of this.projects.values()) {
|
|
613
|
+
if (!project.orgId) locals.push(project);
|
|
614
|
+
else cloud.push({ project, root: vaultFor(project).root });
|
|
615
|
+
}
|
|
616
|
+
if (locals.length > 1) {
|
|
617
|
+
const names = locals.map((p) => `"${p.name}" (${p.id})`).join(", ");
|
|
618
|
+
throw new Error(
|
|
619
|
+
`Refusing to save ${locals.length} local projects in one directory: ${names}. A directory holds a single local project.`
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
const [local] = locals;
|
|
623
|
+
if (local) {
|
|
624
|
+
await ensureLocalVault();
|
|
625
|
+
await ensureDataGitignore(localVaultRoot());
|
|
626
|
+
await this.writeAtomic(this.getFilePath(), JSON.stringify(toDiskForm(local), null, 2));
|
|
627
|
+
} else {
|
|
628
|
+
await rm(this.getFilePath(), { force: true }).catch(() => {
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
await rename2(join2(localVaultRoot(), LEGACY_FILENAME), join2(localVaultRoot(), LEGACY_FILENAME + ".bak")).catch(() => {
|
|
632
|
+
});
|
|
633
|
+
for (const { project, root } of cloud) {
|
|
634
|
+
await mkdir2(root, { recursive: true });
|
|
635
|
+
await this.writeAtomic(this.getFilePath(root), JSON.stringify(toDiskForm(project), null, 2));
|
|
636
|
+
setProjectVault(project.id, root);
|
|
637
|
+
}
|
|
469
638
|
this.events.emit("saved");
|
|
470
639
|
}
|
|
471
640
|
async list() {
|
|
@@ -478,6 +647,12 @@ var ProjectStore = class {
|
|
|
478
647
|
}
|
|
479
648
|
async create(input) {
|
|
480
649
|
await this.load();
|
|
650
|
+
if (!input.orgId) {
|
|
651
|
+
const existing = Array.from(this.projects.values()).find((p) => !p.orgId);
|
|
652
|
+
if (existing) {
|
|
653
|
+
throw new LocalProjectExistsError(existing.name);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
481
656
|
const project = {
|
|
482
657
|
id: randomUUID2(),
|
|
483
658
|
orgId: input.orgId ?? null,
|
|
@@ -489,6 +664,8 @@ var ProjectStore = class {
|
|
|
489
664
|
groupOrder: input.groupOrder ?? []
|
|
490
665
|
};
|
|
491
666
|
this.projects.set(project.id, project);
|
|
667
|
+
setProjectVault(project.id, vaultFor(project).root);
|
|
668
|
+
setProjectServices(project.id, project.serviceIds);
|
|
492
669
|
await this.save();
|
|
493
670
|
return project;
|
|
494
671
|
}
|
|
@@ -507,16 +684,51 @@ var ProjectStore = class {
|
|
|
507
684
|
...input.groupOrder !== void 0 && { groupOrder: input.groupOrder }
|
|
508
685
|
};
|
|
509
686
|
this.projects.set(input.id, updated);
|
|
687
|
+
setProjectVault(updated.id, vaultFor(updated).root);
|
|
688
|
+
setProjectServices(updated.id, updated.serviceIds);
|
|
510
689
|
await this.save();
|
|
511
690
|
return updated;
|
|
512
691
|
}
|
|
513
692
|
async delete(id) {
|
|
514
693
|
await this.load();
|
|
515
|
-
|
|
694
|
+
const project = this.projects.get(id);
|
|
695
|
+
if (!project) return false;
|
|
516
696
|
this.projects.delete(id);
|
|
517
697
|
await this.save();
|
|
698
|
+
await this.discardVault(project);
|
|
518
699
|
return true;
|
|
519
700
|
}
|
|
701
|
+
/**
|
|
702
|
+
* Remove what a deleted cloud project left behind.
|
|
703
|
+
*
|
|
704
|
+
* Its vault is a directory of its own, so dropping the project means dropping
|
|
705
|
+
* the directory — otherwise `readCloudVaults` would resurrect it on the next
|
|
706
|
+
* load. Local projects share their vault with the directory's own files and
|
|
707
|
+
* are only removed from the list.
|
|
708
|
+
*/
|
|
709
|
+
async discardVault(project) {
|
|
710
|
+
forgetProject(project.id);
|
|
711
|
+
if (!project.orgId) return;
|
|
712
|
+
await rm(vaultFor(project).root, { recursive: true, force: true }).catch(() => {
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Stop holding a project on this machine, leaving the doc alone.
|
|
717
|
+
*
|
|
718
|
+
* Memory only: the caller removes the vault itself. Its contents are dropped
|
|
719
|
+
* too, because a service or environment surviving in memory would be written
|
|
720
|
+
* back on the next save — and, its project being unknown by then, into the
|
|
721
|
+
* local vault.
|
|
722
|
+
*/
|
|
723
|
+
async forgetLocally(projectId) {
|
|
724
|
+
await this.load();
|
|
725
|
+
const project = this.projects.get(projectId);
|
|
726
|
+
if (!project) return;
|
|
727
|
+
this.projects.delete(projectId);
|
|
728
|
+
forgetProject(projectId);
|
|
729
|
+
await serviceStore.forgetLocally(project.serviceIds ?? []);
|
|
730
|
+
await environmentStore.forgetByProject(projectId);
|
|
731
|
+
}
|
|
520
732
|
/** All projects linked to a given organization (for the sync bridge). */
|
|
521
733
|
async listByOrg(orgId) {
|
|
522
734
|
await this.load();
|
|
@@ -551,20 +763,26 @@ var ProjectStore = class {
|
|
|
551
763
|
}
|
|
552
764
|
const updated = { ...incoming, activeEnvironmentId: existing.activeEnvironmentId };
|
|
553
765
|
this.projects.set(id, updated);
|
|
766
|
+
setProjectVault(id, vaultFor(updated).root);
|
|
767
|
+
setProjectServices(id, updated.serviceIds);
|
|
554
768
|
await this.save();
|
|
555
769
|
return { project: updated, created: false };
|
|
556
770
|
}
|
|
557
771
|
const project = { ...incoming, activeEnvironmentId: void 0 };
|
|
558
772
|
this.projects.set(id, project);
|
|
773
|
+
setProjectVault(id, vaultFor(project).root);
|
|
774
|
+
setProjectServices(id, project.serviceIds);
|
|
559
775
|
await this.save();
|
|
560
776
|
return { project, created: true };
|
|
561
777
|
}
|
|
562
778
|
/** Delete a project that was removed on another peer. */
|
|
563
779
|
async applyRemoteDelete(id) {
|
|
564
780
|
await this.load();
|
|
565
|
-
|
|
781
|
+
const project = this.projects.get(id);
|
|
782
|
+
if (!project) return false;
|
|
566
783
|
this.projects.delete(id);
|
|
567
784
|
await this.save();
|
|
785
|
+
await this.discardVault(project);
|
|
568
786
|
return true;
|
|
569
787
|
}
|
|
570
788
|
async addService(projectId, serviceId) {
|
|
@@ -574,6 +792,7 @@ var ProjectStore = class {
|
|
|
574
792
|
if (project.serviceIds.includes(serviceId)) return project;
|
|
575
793
|
const updated = { ...project, serviceIds: [...project.serviceIds, serviceId] };
|
|
576
794
|
this.projects.set(projectId, updated);
|
|
795
|
+
setProjectServices(projectId, updated.serviceIds);
|
|
577
796
|
await this.save();
|
|
578
797
|
return updated;
|
|
579
798
|
}
|
|
@@ -583,6 +802,7 @@ var ProjectStore = class {
|
|
|
583
802
|
if (!project) return null;
|
|
584
803
|
const updated = { ...project, serviceIds: project.serviceIds.filter((id) => id !== serviceId) };
|
|
585
804
|
this.projects.set(projectId, updated);
|
|
805
|
+
setProjectServices(projectId, updated.serviceIds);
|
|
586
806
|
await this.save();
|
|
587
807
|
return updated;
|
|
588
808
|
}
|
|
@@ -626,64 +846,115 @@ var ProjectStore = class {
|
|
|
626
846
|
}
|
|
627
847
|
};
|
|
628
848
|
var projectStore = new ProjectStore();
|
|
849
|
+
registerProjectLoader(() => projectStore.list().then(() => void 0));
|
|
629
850
|
|
|
630
851
|
// ../server/src/services/environment-overrides-store.ts
|
|
631
|
-
import { readFile as readFile3, writeFile as
|
|
632
|
-
import { join as join3 } from "path";
|
|
852
|
+
import { readFile as readFile3, writeFile as writeFile4, rename as rename3, mkdir as mkdir3, chmod as chmod3 } from "fs/promises";
|
|
853
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
633
854
|
var FILENAME2 = "environment-overrides.json";
|
|
634
855
|
var EnvironmentOverridesStore = class {
|
|
635
856
|
data = {};
|
|
636
857
|
loaded = false;
|
|
637
858
|
diskEncryptedSlots = {};
|
|
638
859
|
saveQueue = Promise.resolve();
|
|
639
|
-
|
|
640
|
-
|
|
860
|
+
/** Which vault each override was read from, until its project is known. */
|
|
861
|
+
roots = /* @__PURE__ */ new Map();
|
|
862
|
+
/**
|
|
863
|
+
* An override belongs to the project whose variable it answers.
|
|
864
|
+
*
|
|
865
|
+
* Nothing in an override says so — they are keyed by variable id alone — so
|
|
866
|
+
* the owning project is resolved through the environment that defines the
|
|
867
|
+
* variable. Overrides are never synced: they stay in the project's vault on
|
|
868
|
+
* this machine, and no other machine sees them.
|
|
869
|
+
*
|
|
870
|
+
* An override whose variable no longer exists (a deleted environment, a row
|
|
871
|
+
* left behind) has no project to belong to. It stays wherever it was read,
|
|
872
|
+
* falling back to the key's own directory — never dropped, since the variable
|
|
873
|
+
* may simply not be loaded yet.
|
|
874
|
+
*/
|
|
875
|
+
async rootFor(variableId) {
|
|
876
|
+
const projectId = await environmentStore.projectIdOfVariable(variableId);
|
|
877
|
+
if (projectId) return rootOfProject(projectId);
|
|
878
|
+
return this.roots.get(variableId) ?? dirname2(await keyedFilePath(FILENAME2));
|
|
641
879
|
}
|
|
642
|
-
|
|
643
|
-
|
|
880
|
+
/** Where earlier versions kept them: read once, rewritten to their vaults. */
|
|
881
|
+
getLegacyFilePath() {
|
|
882
|
+
return legacyMachineFilePath(FILENAME2);
|
|
644
883
|
}
|
|
884
|
+
/**
|
|
885
|
+
* Read every vault, then the places older versions used.
|
|
886
|
+
*
|
|
887
|
+
* Variable ids are unique, so merging the files cannot collide; a vault read
|
|
888
|
+
* later simply wins over a legacy copy of the same override.
|
|
889
|
+
*/
|
|
645
890
|
async load() {
|
|
646
891
|
if (this.loaded) return;
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
892
|
+
if (!getEncryptionKey()) return;
|
|
893
|
+
this.loaded = true;
|
|
894
|
+
const roots = await allVaultRoots();
|
|
895
|
+
const sources = [
|
|
896
|
+
this.getLegacyFilePath(),
|
|
897
|
+
await keyedFilePath(FILENAME2),
|
|
898
|
+
...roots.map((root) => join3(root, FILENAME2))
|
|
899
|
+
];
|
|
900
|
+
for (const path of sources) {
|
|
901
|
+
let parsed;
|
|
902
|
+
try {
|
|
903
|
+
parsed = JSON.parse(await readFile3(path, "utf-8"));
|
|
904
|
+
} catch {
|
|
905
|
+
continue;
|
|
652
906
|
}
|
|
653
|
-
|
|
654
|
-
|
|
907
|
+
Object.assign(
|
|
908
|
+
this.diskEncryptedSlots,
|
|
909
|
+
extractEncryptedVariableSlots(parsed)
|
|
655
910
|
);
|
|
656
911
|
const decrypted = decryptVariableSlots(
|
|
657
912
|
parsed,
|
|
658
913
|
"environment-overrides"
|
|
659
914
|
);
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
this.data = {};
|
|
915
|
+
for (const [id, override] of Object.entries(decrypted)) {
|
|
916
|
+
this.data[id] = { value: override.value };
|
|
917
|
+
this.roots.set(id, dirname2(path));
|
|
918
|
+
}
|
|
665
919
|
}
|
|
666
|
-
this.loaded = true;
|
|
667
920
|
}
|
|
668
921
|
save() {
|
|
669
922
|
this.saveQueue = this.saveQueue.then(() => this.doSave()).catch(() => this.doSave());
|
|
670
923
|
return this.saveQueue;
|
|
671
924
|
}
|
|
925
|
+
/**
|
|
926
|
+
* Write each vault that holds — or held — an override.
|
|
927
|
+
*
|
|
928
|
+
* A vault emptied by this save is still written, as an empty object: an
|
|
929
|
+
* override that moved to its project's vault must not survive in the one it
|
|
930
|
+
* came from.
|
|
931
|
+
*/
|
|
672
932
|
async doSave() {
|
|
673
|
-
|
|
674
|
-
const
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
this.
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
)
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
933
|
+
const byRoot = /* @__PURE__ */ new Map();
|
|
934
|
+
for (const root of this.roots.values()) byRoot.set(root, {});
|
|
935
|
+
for (const [variableId, override] of Object.entries(this.data)) {
|
|
936
|
+
const root = await this.rootFor(variableId);
|
|
937
|
+
this.roots.set(variableId, root);
|
|
938
|
+
const bucket = byRoot.get(root) ?? {};
|
|
939
|
+
bucket[variableId] = override;
|
|
940
|
+
byRoot.set(root, bucket);
|
|
941
|
+
}
|
|
942
|
+
for (const [root, overrides] of byRoot) {
|
|
943
|
+
const encrypted = encryptVariableSlots(
|
|
944
|
+
overrides,
|
|
945
|
+
this.diskEncryptedSlots
|
|
946
|
+
);
|
|
947
|
+
Object.assign(
|
|
948
|
+
this.diskEncryptedSlots,
|
|
949
|
+
extractEncryptedVariableSlots(encrypted)
|
|
950
|
+
);
|
|
951
|
+
await mkdir3(root, { recursive: true });
|
|
952
|
+
const path = join3(root, FILENAME2);
|
|
953
|
+
const tmpPath = path + ".tmp";
|
|
954
|
+
await writeFile4(tmpPath, JSON.stringify(encrypted, null, 2), "utf-8");
|
|
955
|
+
await rename3(tmpPath, path);
|
|
956
|
+
await chmod3(path, 384);
|
|
957
|
+
}
|
|
687
958
|
}
|
|
688
959
|
async getOverride(variableId) {
|
|
689
960
|
await this.load();
|
|
@@ -1095,16 +1366,15 @@ var environmentResolver = new EnvironmentResolver();
|
|
|
1095
1366
|
|
|
1096
1367
|
// ../server/src/services/audit-service.ts
|
|
1097
1368
|
import { appendFile, readFile as readFile4, stat, rename as rename4, unlink, readdir, mkdir as mkdir4 } from "fs/promises";
|
|
1098
|
-
import { join as join4 } from "path";
|
|
1099
1369
|
var AUDIT_FILENAME = "audit.log";
|
|
1100
1370
|
var MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
1101
1371
|
var MAX_AGE_DAYS = 30;
|
|
1102
1372
|
var AuditService = class {
|
|
1103
1373
|
getFilePath() {
|
|
1104
|
-
return
|
|
1374
|
+
return machineFilePath(AUDIT_FILENAME);
|
|
1105
1375
|
}
|
|
1106
1376
|
getRotatedPath() {
|
|
1107
|
-
return
|
|
1377
|
+
return machineFilePath(`audit.log.${Date.now()}.bak`);
|
|
1108
1378
|
}
|
|
1109
1379
|
/** Append an audit entry (newline-delimited JSON) */
|
|
1110
1380
|
async log(entry) {
|
|
@@ -1115,7 +1385,7 @@ var AuditService = class {
|
|
|
1115
1385
|
const line = JSON.stringify(record) + "\n";
|
|
1116
1386
|
const filepath = this.getFilePath();
|
|
1117
1387
|
try {
|
|
1118
|
-
await mkdir4(
|
|
1388
|
+
await mkdir4(machineRoot(), { recursive: true });
|
|
1119
1389
|
await appendFile(filepath, line, "utf-8");
|
|
1120
1390
|
await this.rotateIfNeeded();
|
|
1121
1391
|
} catch (err) {
|
|
@@ -1168,7 +1438,7 @@ var AuditService = class {
|
|
|
1168
1438
|
}
|
|
1169
1439
|
/** Clean up old rotated logs (older than MAX_AGE_DAYS) */
|
|
1170
1440
|
async cleanup() {
|
|
1171
|
-
const files = await readdir(
|
|
1441
|
+
const files = await readdir(machineRoot());
|
|
1172
1442
|
const cutoff = Date.now() - MAX_AGE_DAYS * 24 * 60 * 60 * 1e3;
|
|
1173
1443
|
for (const file of files) {
|
|
1174
1444
|
if (file.startsWith("audit.log.") && file.endsWith(".bak")) {
|
|
@@ -1176,7 +1446,7 @@ var AuditService = class {
|
|
|
1176
1446
|
const ts = Number.parseInt(tsStr, 10);
|
|
1177
1447
|
if (!Number.isNaN(ts) && ts < cutoff) {
|
|
1178
1448
|
try {
|
|
1179
|
-
await unlink(
|
|
1449
|
+
await unlink(machineFilePath(file));
|
|
1180
1450
|
} catch {
|
|
1181
1451
|
}
|
|
1182
1452
|
}
|
|
@@ -1220,12 +1490,20 @@ var projectRouter = router({
|
|
|
1220
1490
|
serviceIds: z2.array(z2.string().max(128)).max(1e4).optional()
|
|
1221
1491
|
})
|
|
1222
1492
|
).mutation(async ({ input }) => {
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1493
|
+
let project;
|
|
1494
|
+
try {
|
|
1495
|
+
project = await projectStore.create({
|
|
1496
|
+
...input,
|
|
1497
|
+
description: input.description ?? "",
|
|
1498
|
+
serviceIds: input.serviceIds ?? [],
|
|
1499
|
+
environmentIds: []
|
|
1500
|
+
});
|
|
1501
|
+
} catch (err) {
|
|
1502
|
+
if (err instanceof LocalProjectExistsError) {
|
|
1503
|
+
throw new TRPCError2({ code: "CONFLICT", message: err.code, cause: err });
|
|
1504
|
+
}
|
|
1505
|
+
throw new TRPCError2({ code: "CONFLICT", message: err.message });
|
|
1506
|
+
}
|
|
1229
1507
|
const localEnv = await environmentStore.create({
|
|
1230
1508
|
projectId: project.id,
|
|
1231
1509
|
name: "local",
|
|
@@ -1383,8 +1661,7 @@ import { z as z3 } from "zod";
|
|
|
1383
1661
|
import { TRPCError as TRPCError3 } from "@trpc/server";
|
|
1384
1662
|
|
|
1385
1663
|
// ../server/src/services/settings-manager.ts
|
|
1386
|
-
import { readFile as readFile5, writeFile as
|
|
1387
|
-
import { join as join5 } from "path";
|
|
1664
|
+
import { readFile as readFile5, writeFile as writeFile5, rename as rename5, mkdir as mkdir5, chmod as chmod4 } from "fs/promises";
|
|
1388
1665
|
var SETTINGS_FILE = "settings.json";
|
|
1389
1666
|
var SETTINGS_PRIVATE_FILE = "settings.private.json";
|
|
1390
1667
|
var SCHEMA_VERSION_FILE = "schema-version.json";
|
|
@@ -1394,23 +1671,25 @@ var SettingsManager = class {
|
|
|
1394
1671
|
settingsPrivate = null;
|
|
1395
1672
|
schemaVersion = null;
|
|
1396
1673
|
async ensureDir() {
|
|
1397
|
-
await mkdir5(
|
|
1674
|
+
await mkdir5(machineRoot(), { recursive: true });
|
|
1398
1675
|
}
|
|
1399
1676
|
async atomicWrite(filename, data) {
|
|
1400
1677
|
await this.ensureDir();
|
|
1401
|
-
const filePath =
|
|
1678
|
+
const filePath = machineFilePath(filename);
|
|
1402
1679
|
const tmpPath = filePath + ".tmp";
|
|
1403
|
-
await
|
|
1680
|
+
await writeFile5(tmpPath, JSON.stringify(data, null, 2), "utf-8");
|
|
1404
1681
|
await rename5(tmpPath, filePath);
|
|
1405
1682
|
await chmod4(filePath, 384);
|
|
1406
1683
|
}
|
|
1684
|
+
/** Read the machine file, falling back to the launch directory of old. */
|
|
1407
1685
|
async readJson(filename) {
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1686
|
+
for (const path of [machineFilePath(filename), legacyMachineFilePath(filename)]) {
|
|
1687
|
+
try {
|
|
1688
|
+
return JSON.parse(await readFile5(path, "utf-8"));
|
|
1689
|
+
} catch {
|
|
1690
|
+
}
|
|
1413
1691
|
}
|
|
1692
|
+
return null;
|
|
1414
1693
|
}
|
|
1415
1694
|
async getSettings() {
|
|
1416
1695
|
if (!this.settings) {
|
|
@@ -2544,7 +2823,10 @@ var settingsRouter = router({
|
|
|
2544
2823
|
*/
|
|
2545
2824
|
getPaths: protectedProcedure.query(() => ({
|
|
2546
2825
|
cwd: process.cwd(),
|
|
2547
|
-
dataDir: resolve(env.DATA_DIR)
|
|
2826
|
+
dataDir: resolve(env.DATA_DIR),
|
|
2827
|
+
// Where cloud projects live: their vaults belong to the machine, not to
|
|
2828
|
+
// whichever directory the server happened to be started in.
|
|
2829
|
+
machineDir: machineRoot()
|
|
2548
2830
|
})),
|
|
2549
2831
|
/**
|
|
2550
2832
|
* Update settings (mode, cloudUrl)
|
|
@@ -3092,15 +3374,15 @@ import { z as z11 } from "zod";
|
|
|
3092
3374
|
import { TRPCError as TRPCError8 } from "@trpc/server";
|
|
3093
3375
|
import { execFile as execFile2, spawn } from "child_process";
|
|
3094
3376
|
import { stat as stat2 } from "fs/promises";
|
|
3095
|
-
import { resolve as resolve2, join as
|
|
3377
|
+
import { resolve as resolve2, join as join5 } from "path";
|
|
3096
3378
|
import { homedir as homedir2 } from "os";
|
|
3097
3379
|
|
|
3098
3380
|
// ../server/src/services/user-config-manager.ts
|
|
3099
|
-
import { readFile as readFile6, writeFile as
|
|
3100
|
-
import { join as
|
|
3381
|
+
import { readFile as readFile6, writeFile as writeFile6, rename as rename6, mkdir as mkdir6, chmod as chmod5 } from "fs/promises";
|
|
3382
|
+
import { join as join4 } from "path";
|
|
3101
3383
|
import { homedir } from "os";
|
|
3102
3384
|
import { z as z10 } from "zod";
|
|
3103
|
-
var CONFIG_DIR =
|
|
3385
|
+
var CONFIG_DIR = join4(homedir(), ".runeya");
|
|
3104
3386
|
var CONFIG_FILE = "runeya.conf";
|
|
3105
3387
|
var EditorPreferencesSchema = z10.object({
|
|
3106
3388
|
default: z10.string().max(64).optional()
|
|
@@ -3121,16 +3403,16 @@ var UserConfigManager = class {
|
|
|
3121
3403
|
}
|
|
3122
3404
|
async atomicWrite(data) {
|
|
3123
3405
|
await this.ensureDir();
|
|
3124
|
-
const filePath =
|
|
3406
|
+
const filePath = join4(CONFIG_DIR, CONFIG_FILE);
|
|
3125
3407
|
const tmpPath = filePath + ".tmp";
|
|
3126
|
-
await
|
|
3408
|
+
await writeFile6(tmpPath, JSON.stringify(data, null, 2), "utf-8");
|
|
3127
3409
|
await rename6(tmpPath, filePath);
|
|
3128
3410
|
await chmod5(filePath, 384);
|
|
3129
3411
|
}
|
|
3130
3412
|
async getConfig() {
|
|
3131
3413
|
if (!this.config) {
|
|
3132
3414
|
try {
|
|
3133
|
-
const raw = await readFile6(
|
|
3415
|
+
const raw = await readFile6(join4(CONFIG_DIR, CONFIG_FILE), "utf-8");
|
|
3134
3416
|
const parsed = UserConfigSchema.safeParse(JSON.parse(raw));
|
|
3135
3417
|
this.config = parsed.success ? parsed.data : {};
|
|
3136
3418
|
} catch {
|
|
@@ -3266,7 +3548,7 @@ var editorRouter = router({
|
|
|
3266
3548
|
throw new TRPCError8({ code: "BAD_REQUEST", message: `Unknown editor: ${input.editorKey}` });
|
|
3267
3549
|
}
|
|
3268
3550
|
const cwd = await getServiceCwd(input.serviceId);
|
|
3269
|
-
const filePath =
|
|
3551
|
+
const filePath = join5(cwd, input.relativePath);
|
|
3270
3552
|
if (!filePath.startsWith(cwd)) {
|
|
3271
3553
|
throw new TRPCError8({ code: "BAD_REQUEST", message: "Path traversal not allowed" });
|
|
3272
3554
|
}
|
|
@@ -3289,8 +3571,8 @@ var editorRouter = router({
|
|
|
3289
3571
|
// ../server/src/trpc/routers/api-keys.ts
|
|
3290
3572
|
import { z as z12 } from "zod";
|
|
3291
3573
|
import { TRPCError as TRPCError9 } from "@trpc/server";
|
|
3292
|
-
import { mkdir as mkdir7, writeFile as
|
|
3293
|
-
import { join as
|
|
3574
|
+
import { mkdir as mkdir7, writeFile as writeFile7 } from "fs/promises";
|
|
3575
|
+
import { join as join6, dirname as dirname3 } from "path";
|
|
3294
3576
|
var ALLOWED_SKILL_PATHS = /* @__PURE__ */ new Set([
|
|
3295
3577
|
".claude/skills/runeya/SKILL.md",
|
|
3296
3578
|
".cursor/rules/runeya.mdc",
|
|
@@ -3337,9 +3619,9 @@ var apiKeysRouter = router({
|
|
|
3337
3619
|
if (!ALLOWED_SKILL_PATHS.has(input.relativePath)) {
|
|
3338
3620
|
throw new TRPCError9({ code: "BAD_REQUEST", message: "Chemin non autoris\xE9" });
|
|
3339
3621
|
}
|
|
3340
|
-
const abs =
|
|
3341
|
-
await mkdir7(
|
|
3342
|
-
await
|
|
3622
|
+
const abs = join6(process.cwd(), input.relativePath);
|
|
3623
|
+
await mkdir7(dirname3(abs), { recursive: true });
|
|
3624
|
+
await writeFile7(abs, input.content, { encoding: "utf-8", mode: 420 });
|
|
3343
3625
|
return { path: input.relativePath };
|
|
3344
3626
|
})
|
|
3345
3627
|
});
|
|
@@ -3668,27 +3950,54 @@ function buildApiDocs(apiBaseUrl, apiToken) {
|
|
|
3668
3950
|
import { readdir as readdir2, access, readFile as readFile8 } from "fs/promises";
|
|
3669
3951
|
import { watch } from "fs";
|
|
3670
3952
|
import { homedir as homedir3 } from "os";
|
|
3671
|
-
import { join as
|
|
3953
|
+
import { join as join8 } from "path";
|
|
3672
3954
|
|
|
3673
3955
|
// ../server/src/services/conversation-store.ts
|
|
3674
3956
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
3675
|
-
import { readFile as readFile7, writeFile as
|
|
3676
|
-
import { join as
|
|
3957
|
+
import { readFile as readFile7, writeFile as writeFile8, rename as rename7, mkdir as mkdir8, chmod as chmod6 } from "fs/promises";
|
|
3958
|
+
import { dirname as dirname4, join as join7 } from "path";
|
|
3677
3959
|
import { EventEmitter as EventEmitter2 } from "events";
|
|
3678
3960
|
var FILENAME3 = "conversations.private.json";
|
|
3961
|
+
async function localProjectId() {
|
|
3962
|
+
const projects = await projectStore.list().catch(() => []);
|
|
3963
|
+
return projects.find((p) => !p.orgId)?.id ?? null;
|
|
3964
|
+
}
|
|
3679
3965
|
var ConversationStore = class {
|
|
3680
3966
|
conversations = /* @__PURE__ */ new Map();
|
|
3967
|
+
/** Which vault each conversation was read from, until its project is known. */
|
|
3968
|
+
roots = /* @__PURE__ */ new Map();
|
|
3681
3969
|
loaded = false;
|
|
3682
3970
|
events = new EventEmitter2().setMaxListeners(0);
|
|
3683
3971
|
saveQueue = Promise.resolve();
|
|
3684
3972
|
snapshotConversation(conv) {
|
|
3685
3973
|
return structuredClone(conv);
|
|
3686
3974
|
}
|
|
3975
|
+
/**
|
|
3976
|
+
* Encrypted, so it lives beside the key that opens it — usually the machine
|
|
3977
|
+
* root, but a launch directory owning its own key keeps both together.
|
|
3978
|
+
*/
|
|
3687
3979
|
getFilePath() {
|
|
3688
|
-
return
|
|
3980
|
+
return keyedFilePath(FILENAME3);
|
|
3981
|
+
}
|
|
3982
|
+
/** Where older versions kept it — read only, so an older Runeya keeps working. */
|
|
3983
|
+
getLegacyFilePath() {
|
|
3984
|
+
return legacyMachineFilePath(FILENAME3);
|
|
3985
|
+
}
|
|
3986
|
+
/**
|
|
3987
|
+
* Read the machine file, falling back to the launch directory of old.
|
|
3988
|
+
*
|
|
3989
|
+
* The legacy file is left in place: the next save writes the new location,
|
|
3990
|
+
* and a directory still opened by an older Runeya keeps working meanwhile.
|
|
3991
|
+
*/
|
|
3992
|
+
async readFromDisk() {
|
|
3993
|
+
try {
|
|
3994
|
+
return await readFile7(await this.getFilePath(), "utf-8");
|
|
3995
|
+
} catch {
|
|
3996
|
+
return readFile7(this.getLegacyFilePath(), "utf-8");
|
|
3997
|
+
}
|
|
3689
3998
|
}
|
|
3690
3999
|
async ensureDir() {
|
|
3691
|
-
await mkdir8(
|
|
4000
|
+
await mkdir8(dirname4(await this.getFilePath()), { recursive: true });
|
|
3692
4001
|
}
|
|
3693
4002
|
encryptMessages(messages) {
|
|
3694
4003
|
return encryptValue(JSON.stringify(messages));
|
|
@@ -3700,6 +4009,7 @@ var ConversationStore = class {
|
|
|
3700
4009
|
toDisk(conv) {
|
|
3701
4010
|
return {
|
|
3702
4011
|
id: conv.id,
|
|
4012
|
+
projectId: conv.projectId,
|
|
3703
4013
|
title: conv.title,
|
|
3704
4014
|
encryptedMessages: this.encryptMessages(conv.messages),
|
|
3705
4015
|
isCli: conv.isCli,
|
|
@@ -3720,6 +4030,7 @@ var ConversationStore = class {
|
|
|
3720
4030
|
const workflowType = (disk.workflowType === "scenario" ? "scenario" : void 0) ?? (disk.scenarioId ? "scenario" : void 0);
|
|
3721
4031
|
return {
|
|
3722
4032
|
id: disk.id,
|
|
4033
|
+
projectId: disk.projectId,
|
|
3723
4034
|
title: disk.title,
|
|
3724
4035
|
messages: this.decryptMessages(disk.encryptedMessages),
|
|
3725
4036
|
isCli: disk.isCli ?? disk.isClaudeCode ?? false,
|
|
@@ -3736,33 +4047,81 @@ var ConversationStore = class {
|
|
|
3736
4047
|
updatedAt: disk.updatedAt
|
|
3737
4048
|
};
|
|
3738
4049
|
}
|
|
4050
|
+
/**
|
|
4051
|
+
* Read every project vault, then the homes older versions used.
|
|
4052
|
+
*
|
|
4053
|
+
* A conversation found in a legacy file is attributed to the local project of
|
|
4054
|
+
* that directory — which is what it was about, back when one directory meant
|
|
4055
|
+
* one project. With no local project to point at, it is left unattributed
|
|
4056
|
+
* rather than filed under a project picked at random.
|
|
4057
|
+
*/
|
|
3739
4058
|
async load() {
|
|
3740
4059
|
if (this.loaded) return;
|
|
3741
4060
|
if (!getEncryptionKey()) {
|
|
3742
4061
|
return;
|
|
3743
4062
|
}
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
4063
|
+
this.loaded = true;
|
|
4064
|
+
const vaults = await allVaultRoots();
|
|
4065
|
+
const legacyPaths = [this.getLegacyFilePath(), await keyedFilePath(FILENAME3)];
|
|
4066
|
+
const sources = [
|
|
4067
|
+
...legacyPaths.map((path) => ({ path, legacy: true })),
|
|
4068
|
+
...vaults.map((root) => ({ path: join7(root, FILENAME3), legacy: false }))
|
|
4069
|
+
];
|
|
4070
|
+
for (const { path, legacy } of sources) {
|
|
4071
|
+
let data;
|
|
4072
|
+
try {
|
|
4073
|
+
data = JSON.parse(await readFile7(path, "utf-8"));
|
|
4074
|
+
} catch {
|
|
4075
|
+
continue;
|
|
4076
|
+
}
|
|
4077
|
+
const inherited = legacy ? await localProjectId() : null;
|
|
3747
4078
|
for (const item of data) {
|
|
3748
4079
|
try {
|
|
3749
|
-
|
|
4080
|
+
const conv = this.fromDisk(item);
|
|
4081
|
+
if (!conv.projectId && inherited) conv.projectId = inherited;
|
|
4082
|
+
this.conversations.set(item.id, conv);
|
|
4083
|
+
this.roots.set(item.id, dirname4(path));
|
|
3750
4084
|
} catch {
|
|
3751
4085
|
}
|
|
3752
4086
|
}
|
|
3753
|
-
} catch {
|
|
3754
4087
|
}
|
|
3755
|
-
this.loaded = true;
|
|
3756
4088
|
}
|
|
4089
|
+
/**
|
|
4090
|
+
* Write each vault that holds — or held — a conversation.
|
|
4091
|
+
*
|
|
4092
|
+
* A vault emptied by this save is still written, as an empty list: a
|
|
4093
|
+
* conversation that found its project must not survive in the file it was
|
|
4094
|
+
* read from.
|
|
4095
|
+
*/
|
|
3757
4096
|
async saveInternal() {
|
|
3758
|
-
await this.ensureDir();
|
|
3759
4097
|
if (!getEncryptionKey()) return;
|
|
3760
|
-
const
|
|
3761
|
-
const
|
|
3762
|
-
const
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
4098
|
+
const byRoot = /* @__PURE__ */ new Map();
|
|
4099
|
+
for (const root of this.roots.values()) byRoot.set(root, []);
|
|
4100
|
+
for (const conv of this.conversations.values()) {
|
|
4101
|
+
const root = await this.rootFor(conv);
|
|
4102
|
+
this.roots.set(conv.id, root);
|
|
4103
|
+
const bucket = byRoot.get(root) ?? [];
|
|
4104
|
+
bucket.push(this.toDisk(conv));
|
|
4105
|
+
byRoot.set(root, bucket);
|
|
4106
|
+
}
|
|
4107
|
+
for (const [root, data] of byRoot) {
|
|
4108
|
+
await mkdir8(root, { recursive: true });
|
|
4109
|
+
const filePath = join7(root, FILENAME3);
|
|
4110
|
+
const tmpPath = `${filePath}.tmp`;
|
|
4111
|
+
await writeFile8(tmpPath, JSON.stringify(data, null, 2), "utf-8");
|
|
4112
|
+
await rename7(tmpPath, filePath);
|
|
4113
|
+
await chmod6(filePath, 384);
|
|
4114
|
+
}
|
|
4115
|
+
}
|
|
4116
|
+
/**
|
|
4117
|
+
* The vault a conversation is written to: its project's.
|
|
4118
|
+
*
|
|
4119
|
+
* One that names no project stays where it was read, falling back to the
|
|
4120
|
+
* key's own directory — never lost for want of an owner.
|
|
4121
|
+
*/
|
|
4122
|
+
async rootFor(conv) {
|
|
4123
|
+
if (conv.projectId) return rootOfProject(conv.projectId);
|
|
4124
|
+
return this.roots.get(conv.id) ?? dirname4(await keyedFilePath(FILENAME3));
|
|
3766
4125
|
}
|
|
3767
4126
|
async save() {
|
|
3768
4127
|
const write = this.saveQueue.then(() => this.saveInternal());
|
|
@@ -3778,11 +4137,12 @@ var ConversationStore = class {
|
|
|
3778
4137
|
await this.load();
|
|
3779
4138
|
return this.conversations.get(id) ?? null;
|
|
3780
4139
|
}
|
|
3781
|
-
async create(title, isCli = false, workflowId, workflowType, runner, kanbanBoardId, kanbanCardId, providerId) {
|
|
4140
|
+
async create(title, isCli = false, workflowId, workflowType, runner, kanbanBoardId, kanbanCardId, providerId, projectId) {
|
|
3782
4141
|
await this.load();
|
|
3783
4142
|
const now = Date.now();
|
|
3784
4143
|
const conv = {
|
|
3785
4144
|
id: randomUUID3(),
|
|
4145
|
+
projectId,
|
|
3786
4146
|
title: title ?? "New conversation",
|
|
3787
4147
|
messages: [],
|
|
3788
4148
|
isCli,
|
|
@@ -3858,7 +4218,7 @@ var ConversationStore = class {
|
|
|
3858
4218
|
var conversationStore = new ConversationStore();
|
|
3859
4219
|
|
|
3860
4220
|
// ../server/src/services/claude-transcript-sync.ts
|
|
3861
|
-
var PROJECTS_DIR =
|
|
4221
|
+
var PROJECTS_DIR = join8(homedir3(), ".claude", "projects");
|
|
3862
4222
|
var DEFAULT_TITLES = /* @__PURE__ */ new Set(["", "New conversation", "Nouvelle conversation"]);
|
|
3863
4223
|
async function findClaudeSessionFile(sessionId) {
|
|
3864
4224
|
let entries;
|
|
@@ -3869,7 +4229,7 @@ async function findClaudeSessionFile(sessionId) {
|
|
|
3869
4229
|
}
|
|
3870
4230
|
const filename = `${sessionId}.jsonl`;
|
|
3871
4231
|
for (const dir of entries) {
|
|
3872
|
-
const candidate =
|
|
4232
|
+
const candidate = join8(PROJECTS_DIR, dir, filename);
|
|
3873
4233
|
try {
|
|
3874
4234
|
await access(candidate);
|
|
3875
4235
|
return candidate;
|
|
@@ -4032,7 +4392,7 @@ async function startClaudeTranscriptWatcher() {
|
|
|
4032
4392
|
}
|
|
4033
4393
|
for (const dir of topLevel) {
|
|
4034
4394
|
try {
|
|
4035
|
-
const w = watch(
|
|
4395
|
+
const w = watch(join8(PROJECTS_DIR, dir), (_event, filename) => {
|
|
4036
4396
|
handleFsEvent(filename ? filename.toString() : null);
|
|
4037
4397
|
});
|
|
4038
4398
|
w.on("error", () => {
|
|
@@ -4058,8 +4418,8 @@ function stopClaudeTranscriptWatcher() {
|
|
|
4058
4418
|
// ../server/src/services/codex-sessions.ts
|
|
4059
4419
|
import { readdir as readdir3 } from "fs/promises";
|
|
4060
4420
|
import { homedir as homedir4 } from "os";
|
|
4061
|
-
import { join as
|
|
4062
|
-
var SESSIONS_DIR =
|
|
4421
|
+
import { join as join9 } from "path";
|
|
4422
|
+
var SESSIONS_DIR = join9(homedir4(), ".codex", "sessions");
|
|
4063
4423
|
async function findCodexSessionFile(sessionId) {
|
|
4064
4424
|
const suffix = `-${sessionId}.jsonl`;
|
|
4065
4425
|
async function walk(dir) {
|
|
@@ -4070,7 +4430,7 @@ async function findCodexSessionFile(sessionId) {
|
|
|
4070
4430
|
return null;
|
|
4071
4431
|
}
|
|
4072
4432
|
for (const entry of entries) {
|
|
4073
|
-
const full =
|
|
4433
|
+
const full = join9(dir, entry.name);
|
|
4074
4434
|
if (entry.isDirectory()) {
|
|
4075
4435
|
const found = await walk(full);
|
|
4076
4436
|
if (found) return found;
|
|
@@ -5030,19 +5390,18 @@ var ScenarioStore = class {
|
|
|
5030
5390
|
var scenarioStore = new ScenarioStore();
|
|
5031
5391
|
|
|
5032
5392
|
// ../server/src/services/scenario-run-store.ts
|
|
5033
|
-
import { mkdir as mkdir10, rm, readFile as readFile10 } from "fs/promises";
|
|
5034
|
-
import { join as
|
|
5393
|
+
import { mkdir as mkdir10, rm as rm2, readFile as readFile10 } from "fs/promises";
|
|
5394
|
+
import { join as join10 } from "path";
|
|
5035
5395
|
|
|
5036
5396
|
// ../server/src/services/run-store-utils.ts
|
|
5037
|
-
import { chmod as chmod7, mkdir as mkdir9, rename as rename8, writeFile as
|
|
5038
|
-
import { dirname as
|
|
5039
|
-
import { readFile as readFile9 } from "fs/promises";
|
|
5397
|
+
import { chmod as chmod7, mkdir as mkdir9, rename as rename8, writeFile as writeFile9, readFile as readFile9 } from "fs/promises";
|
|
5398
|
+
import { dirname as dirname5, resolve as resolve3, sep } from "path";
|
|
5040
5399
|
var SAFE_PATH_SEGMENT_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;
|
|
5041
5400
|
function resolveConversationRunDir(rootDirName, conversationId) {
|
|
5042
5401
|
if (!SAFE_PATH_SEGMENT_RE.test(conversationId)) {
|
|
5043
5402
|
throw new Error("Invalid conversationId");
|
|
5044
5403
|
}
|
|
5045
|
-
const base = resolve3(
|
|
5404
|
+
const base = resolve3(machineRoot(), rootDirName);
|
|
5046
5405
|
const dir = resolve3(base, conversationId);
|
|
5047
5406
|
if (!dir.startsWith(base + sep)) {
|
|
5048
5407
|
throw new Error("Invalid conversationId");
|
|
@@ -5050,9 +5409,9 @@ function resolveConversationRunDir(rootDirName, conversationId) {
|
|
|
5050
5409
|
return dir;
|
|
5051
5410
|
}
|
|
5052
5411
|
async function atomicWriteSecure(filePath, content) {
|
|
5053
|
-
await mkdir9(
|
|
5412
|
+
await mkdir9(dirname5(filePath), { recursive: true });
|
|
5054
5413
|
const tmpPath = `${filePath}.tmp`;
|
|
5055
|
-
await
|
|
5414
|
+
await writeFile9(tmpPath, content, "utf-8");
|
|
5056
5415
|
await rename8(tmpPath, filePath);
|
|
5057
5416
|
await chmod7(filePath, 384);
|
|
5058
5417
|
}
|
|
@@ -5109,8 +5468,8 @@ var ScenarioRunStore = class _ScenarioRunStore {
|
|
|
5109
5468
|
"- Use read-only references from linked upstream files as context.",
|
|
5110
5469
|
""
|
|
5111
5470
|
].join("\n");
|
|
5112
|
-
await atomicWriteSecure(
|
|
5113
|
-
await atomicWriteSecure(
|
|
5471
|
+
await atomicWriteSecure(join10(dir, "README.md"), overview);
|
|
5472
|
+
await atomicWriteSecure(join10(dir, "USER_REQUEST.md"), `# User request
|
|
5114
5473
|
|
|
5115
5474
|
${userInput.trim() || "(empty)"}
|
|
5116
5475
|
`);
|
|
@@ -5136,11 +5495,11 @@ ${userInput.trim() || "(empty)"}
|
|
|
5136
5495
|
"- Waiting for assignment.",
|
|
5137
5496
|
""
|
|
5138
5497
|
].join("\n");
|
|
5139
|
-
await atomicWriteSecure(
|
|
5498
|
+
await atomicWriteSecure(join10(dir, fileName), content);
|
|
5140
5499
|
}
|
|
5141
5500
|
}
|
|
5142
5501
|
async deleteRunDir(conversationId) {
|
|
5143
|
-
await
|
|
5502
|
+
await rm2(this.getRunDir(conversationId), { recursive: true, force: true });
|
|
5144
5503
|
this.activeRuns.delete(conversationId);
|
|
5145
5504
|
}
|
|
5146
5505
|
ensureRunState(conversationId) {
|
|
@@ -5204,18 +5563,18 @@ ${userInput.trim() || "(empty)"}
|
|
|
5204
5563
|
...Object.keys(runState.nodeAgentIds).length > 0 ? { nodeAgentIds: { ...runState.nodeAgentIds } } : {},
|
|
5205
5564
|
...Object.keys(runState.waitingInputs).length > 0 ? { waitingInputs: { ...runState.waitingInputs } } : {}
|
|
5206
5565
|
};
|
|
5207
|
-
await atomicWriteJsonSecure(
|
|
5566
|
+
await atomicWriteJsonSecure(join10(dir, "run-state.json"), data);
|
|
5208
5567
|
} catch {
|
|
5209
5568
|
}
|
|
5210
5569
|
}
|
|
5211
5570
|
async loadPersistedRunState(conversationId) {
|
|
5212
5571
|
const dir = this.getRunDir(conversationId);
|
|
5213
|
-
return readJsonFileSafe(
|
|
5572
|
+
return readJsonFileSafe(join10(dir, "run-state.json"));
|
|
5214
5573
|
}
|
|
5215
5574
|
async readUserRequest(conversationId) {
|
|
5216
5575
|
try {
|
|
5217
5576
|
const dir = this.getRunDir(conversationId);
|
|
5218
|
-
const content = await readFile10(
|
|
5577
|
+
const content = await readFile10(join10(dir, "USER_REQUEST.md"), "utf-8");
|
|
5219
5578
|
return content.replace(/^#\s*User request\s*\n+/, "").trim() || null;
|
|
5220
5579
|
} catch {
|
|
5221
5580
|
return null;
|
|
@@ -5224,7 +5583,7 @@ ${userInput.trim() || "(empty)"}
|
|
|
5224
5583
|
async readReadme(conversationId) {
|
|
5225
5584
|
try {
|
|
5226
5585
|
const dir = this.getRunDir(conversationId);
|
|
5227
|
-
return await readFile10(
|
|
5586
|
+
return await readFile10(join10(dir, "README.md"), "utf-8");
|
|
5228
5587
|
} catch {
|
|
5229
5588
|
return null;
|
|
5230
5589
|
}
|
|
@@ -5233,14 +5592,14 @@ ${userInput.trim() || "(empty)"}
|
|
|
5233
5592
|
const dir = this.getRunDir(conversationId);
|
|
5234
5593
|
await mkdir10(dir, { recursive: true });
|
|
5235
5594
|
await atomicWriteJsonSecure(
|
|
5236
|
-
|
|
5595
|
+
join10(dir, _ScenarioRunStore.SCENARIO_SNAPSHOT_FILE),
|
|
5237
5596
|
scenario,
|
|
5238
5597
|
true
|
|
5239
5598
|
);
|
|
5240
5599
|
}
|
|
5241
5600
|
async loadScenarioSnapshot(conversationId) {
|
|
5242
5601
|
const dir = this.getRunDir(conversationId);
|
|
5243
|
-
return readJsonFileSafe(
|
|
5602
|
+
return readJsonFileSafe(join10(dir, _ScenarioRunStore.SCENARIO_SNAPSHOT_FILE));
|
|
5244
5603
|
}
|
|
5245
5604
|
};
|
|
5246
5605
|
var scenarioRunStore = new ScenarioRunStore();
|
|
@@ -5286,9 +5645,9 @@ async function fetchProviderModels(provider, plaintextKey) {
|
|
|
5286
5645
|
|
|
5287
5646
|
// ../server/src/services/image-storage.service.ts
|
|
5288
5647
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
5289
|
-
import { mkdir as mkdir11, writeFile as
|
|
5648
|
+
import { mkdir as mkdir11, writeFile as writeFile10, readFile as readFile11, unlink as unlink2, readdir as readdir4 } from "fs/promises";
|
|
5290
5649
|
import { existsSync } from "fs";
|
|
5291
|
-
import { join as
|
|
5650
|
+
import { join as join11, extname, resolve as resolve4, sep as sep2 } from "path";
|
|
5292
5651
|
var ALLOWED_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
5293
5652
|
"image/png",
|
|
5294
5653
|
"image/jpeg",
|
|
@@ -5325,15 +5684,15 @@ var ImageStorageService = class {
|
|
|
5325
5684
|
const imageId = randomUUID4();
|
|
5326
5685
|
const safeExt = ext.replace(/[^a-z.]/g, "");
|
|
5327
5686
|
const fileName = `${imageId}${safeExt}`;
|
|
5328
|
-
const filePath =
|
|
5687
|
+
const filePath = join11(this.dir, fileName);
|
|
5329
5688
|
const resolvedPath = resolve4(filePath);
|
|
5330
5689
|
const resolvedDir = resolve4(this.dir);
|
|
5331
5690
|
if (resolvedPath !== resolvedDir && !resolvedPath.startsWith(resolvedDir + sep2)) {
|
|
5332
5691
|
throw new Error("Invalid file path");
|
|
5333
5692
|
}
|
|
5334
|
-
await
|
|
5693
|
+
await writeFile10(filePath, buffer, { mode: 384 });
|
|
5335
5694
|
const meta = { imageId, filePath, mimeType, originalName, createdAt: Date.now(), contextId };
|
|
5336
|
-
await
|
|
5695
|
+
await writeFile10(`${filePath}.meta.json`, JSON.stringify(meta), { encoding: "utf-8", mode: 384 });
|
|
5337
5696
|
return meta;
|
|
5338
5697
|
}
|
|
5339
5698
|
/**
|
|
@@ -5351,7 +5710,7 @@ var ImageStorageService = class {
|
|
|
5351
5710
|
const files = await readdir4(this.dir);
|
|
5352
5711
|
const metaFile = files.find((f) => f.startsWith(imageId) && f.endsWith(".meta.json"));
|
|
5353
5712
|
if (!metaFile) return null;
|
|
5354
|
-
const raw = JSON.parse(await readFile11(
|
|
5713
|
+
const raw = JSON.parse(await readFile11(join11(this.dir, metaFile), "utf-8"));
|
|
5355
5714
|
if (expectedContextId !== void 0 && raw.contextId !== expectedContextId) return null;
|
|
5356
5715
|
if (!existsSync(raw.filePath)) return null;
|
|
5357
5716
|
return { filePath: raw.filePath, mimeType: raw.mimeType, contextId: raw.contextId };
|
|
@@ -5377,7 +5736,7 @@ var ImageStorageService = class {
|
|
|
5377
5736
|
await Promise.all(
|
|
5378
5737
|
files.filter((f) => f.endsWith(".meta.json")).map(async (file) => {
|
|
5379
5738
|
try {
|
|
5380
|
-
const raw = JSON.parse(await readFile11(
|
|
5739
|
+
const raw = JSON.parse(await readFile11(join11(this.dir, file), "utf-8"));
|
|
5381
5740
|
if (raw.contextId === contextId) {
|
|
5382
5741
|
await unlink2(raw.filePath).catch(() => {
|
|
5383
5742
|
});
|
|
@@ -5407,7 +5766,7 @@ var ImageStorageService = class {
|
|
|
5407
5766
|
for (const file of files) {
|
|
5408
5767
|
if (!file.endsWith(".meta.json")) continue;
|
|
5409
5768
|
try {
|
|
5410
|
-
const raw = JSON.parse(await readFile11(
|
|
5769
|
+
const raw = JSON.parse(await readFile11(join11(this.dir, file), "utf-8"));
|
|
5411
5770
|
if (now - raw.createdAt > ttlMs) expired.push(raw);
|
|
5412
5771
|
} catch {
|
|
5413
5772
|
}
|
|
@@ -5418,8 +5777,7 @@ var ImageStorageService = class {
|
|
|
5418
5777
|
var _singleton;
|
|
5419
5778
|
function getSingleton() {
|
|
5420
5779
|
if (!_singleton) {
|
|
5421
|
-
|
|
5422
|
-
_singleton = new ImageStorageService(resolve4(dataDir, "images"));
|
|
5780
|
+
_singleton = new ImageStorageService(resolve4(machineRoot(), "images"));
|
|
5423
5781
|
}
|
|
5424
5782
|
return _singleton;
|
|
5425
5783
|
}
|
|
@@ -5480,9 +5838,9 @@ import { basename } from "path";
|
|
|
5480
5838
|
import { TRPCError as TRPCError10 } from "@trpc/server";
|
|
5481
5839
|
|
|
5482
5840
|
// ../server/src/services/cloud-session-store.ts
|
|
5483
|
-
import { readFile as readFile12, writeFile as
|
|
5841
|
+
import { readFile as readFile12, writeFile as writeFile11, rename as rename9, mkdir as mkdir12, chmod as chmod8, unlink as unlink3 } from "fs/promises";
|
|
5842
|
+
import { dirname as dirname6 } from "path";
|
|
5484
5843
|
import { randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
5485
|
-
import { join as join14 } from "path";
|
|
5486
5844
|
var FILENAME4 = "cloud-session.json";
|
|
5487
5845
|
var STATE_TTL_MS = 5 * 60 * 1e3;
|
|
5488
5846
|
var CloudSessionStore = class {
|
|
@@ -5490,13 +5848,34 @@ var CloudSessionStore = class {
|
|
|
5490
5848
|
loaded = false;
|
|
5491
5849
|
/** Pending login states (CSRF binding between `start` and the callback). */
|
|
5492
5850
|
pending = /* @__PURE__ */ new Map();
|
|
5851
|
+
/**
|
|
5852
|
+
* Encrypted, so it lives beside the key that opens it — usually the machine
|
|
5853
|
+
* root, but a launch directory owning its own key keeps both together.
|
|
5854
|
+
*/
|
|
5493
5855
|
getFilePath() {
|
|
5494
|
-
return
|
|
5856
|
+
return keyedFilePath(FILENAME4);
|
|
5857
|
+
}
|
|
5858
|
+
/** Where older versions kept it — read only, so an older Runeya keeps working. */
|
|
5859
|
+
getLegacyFilePath() {
|
|
5860
|
+
return legacyMachineFilePath(FILENAME4);
|
|
5861
|
+
}
|
|
5862
|
+
/**
|
|
5863
|
+
* Read the machine file, falling back to the launch directory of old.
|
|
5864
|
+
*
|
|
5865
|
+
* The legacy file is left in place: the next save writes the new location,
|
|
5866
|
+
* and a directory still opened by an older Runeya keeps working meanwhile.
|
|
5867
|
+
*/
|
|
5868
|
+
async readFromDisk() {
|
|
5869
|
+
try {
|
|
5870
|
+
return await readFile12(await this.getFilePath(), "utf-8");
|
|
5871
|
+
} catch {
|
|
5872
|
+
return readFile12(this.getLegacyFilePath(), "utf-8");
|
|
5873
|
+
}
|
|
5495
5874
|
}
|
|
5496
5875
|
async load() {
|
|
5497
5876
|
if (this.loaded) return;
|
|
5498
5877
|
try {
|
|
5499
|
-
const raw = await
|
|
5878
|
+
const raw = await this.readFromDisk();
|
|
5500
5879
|
this.session = JSON.parse(raw);
|
|
5501
5880
|
} catch {
|
|
5502
5881
|
}
|
|
@@ -5537,10 +5916,10 @@ var CloudSessionStore = class {
|
|
|
5537
5916
|
}
|
|
5538
5917
|
}
|
|
5539
5918
|
async save() {
|
|
5540
|
-
await mkdir12(
|
|
5541
|
-
const filePath = this.getFilePath();
|
|
5919
|
+
await mkdir12(dirname6(await this.getFilePath()), { recursive: true });
|
|
5920
|
+
const filePath = await this.getFilePath();
|
|
5542
5921
|
const tmpPath = filePath + ".tmp";
|
|
5543
|
-
await
|
|
5922
|
+
await writeFile11(tmpPath, JSON.stringify(this.session, null, 2), "utf-8");
|
|
5544
5923
|
await rename9(tmpPath, filePath);
|
|
5545
5924
|
await chmod8(filePath, 384);
|
|
5546
5925
|
}
|
|
@@ -5569,7 +5948,7 @@ var CloudSessionStore = class {
|
|
|
5569
5948
|
this.session = null;
|
|
5570
5949
|
this.pending.clear();
|
|
5571
5950
|
try {
|
|
5572
|
-
await unlink3(this.getFilePath());
|
|
5951
|
+
await unlink3(await this.getFilePath());
|
|
5573
5952
|
} catch {
|
|
5574
5953
|
}
|
|
5575
5954
|
}
|
|
@@ -6211,7 +6590,7 @@ function tailTranscript(opts) {
|
|
|
6211
6590
|
|
|
6212
6591
|
// ../server/src/services/ai-runners/claude-pty-runner.ts
|
|
6213
6592
|
async function getAgentManager() {
|
|
6214
|
-
const { agentManager: agentManager2 } = await import("./agent-manager-
|
|
6593
|
+
const { agentManager: agentManager2 } = await import("./agent-manager-2I47P7CA.js");
|
|
6215
6594
|
return agentManager2;
|
|
6216
6595
|
}
|
|
6217
6596
|
function buildPtyArgs(params, sessionId, opts) {
|
|
@@ -6864,7 +7243,7 @@ async function shutdownCodexAppServer() {
|
|
|
6864
7243
|
}
|
|
6865
7244
|
}
|
|
6866
7245
|
async function getAgentManager2() {
|
|
6867
|
-
const { agentManager: agentManager2 } = await import("./agent-manager-
|
|
7246
|
+
const { agentManager: agentManager2 } = await import("./agent-manager-2I47P7CA.js");
|
|
6868
7247
|
return agentManager2;
|
|
6869
7248
|
}
|
|
6870
7249
|
function parseRuneyaCurlCommand(command) {
|
|
@@ -7745,7 +8124,20 @@ var chatRouter = router({
|
|
|
7745
8124
|
conversationStore.offChanged(handler);
|
|
7746
8125
|
}
|
|
7747
8126
|
}),
|
|
7748
|
-
|
|
8127
|
+
/**
|
|
8128
|
+
* Conversations of one project.
|
|
8129
|
+
*
|
|
8130
|
+
* A conversation belongs to the project it was opened from, so the caller
|
|
8131
|
+
* says which one it is looking at. Ones that name no project — written
|
|
8132
|
+
* before the field existed, or that a migration could not attribute — are
|
|
8133
|
+
* returned whatever the project: hiding them would make them unreachable.
|
|
8134
|
+
*/
|
|
8135
|
+
list: protectedProcedure.input(z15.object({ projectId: Id128Schema.optional() }).optional()).query(async ({ input }) => {
|
|
8136
|
+
const all = await conversationStore.list();
|
|
8137
|
+
const projectId = input?.projectId;
|
|
8138
|
+
if (!projectId) return all;
|
|
8139
|
+
return all.filter((c) => !c.projectId || c.projectId === projectId);
|
|
8140
|
+
}),
|
|
7749
8141
|
get: protectedProcedure.input(z15.object({ id: Id128Schema })).query(async ({ input }) => {
|
|
7750
8142
|
const conv = await conversationStore.get(input.id);
|
|
7751
8143
|
if (!conv) throw new TRPCError11({ code: "NOT_FOUND", message: "Conversation not found" });
|
|
@@ -7759,7 +8151,10 @@ var chatRouter = router({
|
|
|
7759
8151
|
scenarioId: Id128Schema.optional(),
|
|
7760
8152
|
scenarioInput: z15.string().max(1e5).optional(),
|
|
7761
8153
|
kanbanBoardId: Id128Schema.optional(),
|
|
7762
|
-
kanbanCardId: Id128Schema.optional()
|
|
8154
|
+
kanbanCardId: Id128Schema.optional(),
|
|
8155
|
+
// The project the conversation is about: only the caller knows which
|
|
8156
|
+
// one it is looking at.
|
|
8157
|
+
projectId: Id128Schema.optional()
|
|
7763
8158
|
})).mutation(async ({ input }) => {
|
|
7764
8159
|
let scenario = null;
|
|
7765
8160
|
if (input.scenarioId) {
|
|
@@ -7774,7 +8169,8 @@ var chatRouter = router({
|
|
|
7774
8169
|
input.runner,
|
|
7775
8170
|
input.kanbanBoardId,
|
|
7776
8171
|
input.kanbanCardId,
|
|
7777
|
-
input.providerId
|
|
8172
|
+
input.providerId,
|
|
8173
|
+
input.projectId
|
|
7778
8174
|
);
|
|
7779
8175
|
if (scenario) {
|
|
7780
8176
|
await scenarioRunStore.initializeConversationWorkspace(
|
|
@@ -8996,7 +9392,7 @@ async function* streamWithActiveProvider(opts) {
|
|
|
8996
9392
|
// ../server/src/services/scenario-runner.ts
|
|
8997
9393
|
import { mkdir as mkdir13 } from "fs/promises";
|
|
8998
9394
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
8999
|
-
import { join as
|
|
9395
|
+
import { join as join12 } from "path";
|
|
9000
9396
|
|
|
9001
9397
|
// ../server/src/services/scenario-presets.ts
|
|
9002
9398
|
var BUILT_IN_PRESETS = [
|
|
@@ -9770,7 +10166,7 @@ async function appendNodeOutput(conversationId, node, output) {
|
|
|
9770
10166
|
if (!dir) return;
|
|
9771
10167
|
const slug = node.label.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 60) || "member";
|
|
9772
10168
|
const safeId = node.id.replace(/[^a-z0-9_-]/gi, "-").slice(0, 80);
|
|
9773
|
-
const filePath =
|
|
10169
|
+
const filePath = join12(dir, `${slug}_${safeId}.md`);
|
|
9774
10170
|
const content = `## Output
|
|
9775
10171
|
|
|
9776
10172
|
${output}
|
|
@@ -9785,7 +10181,7 @@ async function appendToReadme(conversationId, bus, text) {
|
|
|
9785
10181
|
try {
|
|
9786
10182
|
await mkdir13(dir, { recursive: true });
|
|
9787
10183
|
const { appendFile: appendFile2 } = await import("fs/promises");
|
|
9788
|
-
await appendFile2(
|
|
10184
|
+
await appendFile2(join12(dir, "README.md"), line, "utf-8");
|
|
9789
10185
|
} catch {
|
|
9790
10186
|
}
|
|
9791
10187
|
publishToBus(bus, { type: "readme_append", text: line });
|
|
@@ -9805,7 +10201,7 @@ async function writeFinalMd(conversationId, bus, userRequest, content) {
|
|
|
9805
10201
|
|
|
9806
10202
|
${content}
|
|
9807
10203
|
`;
|
|
9808
|
-
const filePath =
|
|
10204
|
+
const filePath = join12(dir, "FINAL.md");
|
|
9809
10205
|
await atomicWriteSecure(filePath, fullContent);
|
|
9810
10206
|
await appendToReadme(
|
|
9811
10207
|
conversationId,
|
|
@@ -10984,10 +11380,10 @@ var kanbanQueueRouter = router({
|
|
|
10984
11380
|
});
|
|
10985
11381
|
|
|
10986
11382
|
// ../server/src/trpc/routers/kanban.ts
|
|
10987
|
-
import { mkdir as mkdir14, readdir as readdir5, rm as
|
|
11383
|
+
import { mkdir as mkdir14, readdir as readdir5, rm as rm3, stat as stat3, writeFile as writeFile12 } from "fs/promises";
|
|
10988
11384
|
import { createHash } from "crypto";
|
|
10989
11385
|
import { tmpdir } from "os";
|
|
10990
|
-
import { join as
|
|
11386
|
+
import { join as join13 } from "path";
|
|
10991
11387
|
import { z as z20 } from "zod";
|
|
10992
11388
|
import { TRPCError as TRPCError17 } from "@trpc/server";
|
|
10993
11389
|
function summarizeCard(card, boardId, columnTitle) {
|
|
@@ -11038,30 +11434,30 @@ function safeDecode(value) {
|
|
|
11038
11434
|
}
|
|
11039
11435
|
var MAX_ATTACHMENT_BYTES = 55 * 1024 * 1024;
|
|
11040
11436
|
function attachmentCacheDir() {
|
|
11041
|
-
return
|
|
11437
|
+
return join13(tmpdir(), "runeya-kanban-attachments");
|
|
11042
11438
|
}
|
|
11043
11439
|
var ATTACHMENT_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
11044
11440
|
async function purgeStaleAttachments(dir, ttlMs = ATTACHMENT_CACHE_TTL_MS) {
|
|
11045
11441
|
try {
|
|
11046
11442
|
const cutoff = Date.now() - ttlMs;
|
|
11047
11443
|
for (const name of await readdir5(dir)) {
|
|
11048
|
-
const path =
|
|
11444
|
+
const path = join13(dir, name);
|
|
11049
11445
|
const info = await stat3(path).catch(() => null);
|
|
11050
|
-
if (info && info.mtimeMs < cutoff) await
|
|
11446
|
+
if (info && info.mtimeMs < cutoff) await rm3(path, { force: true });
|
|
11051
11447
|
}
|
|
11052
11448
|
} catch {
|
|
11053
11449
|
}
|
|
11054
11450
|
}
|
|
11055
11451
|
function attachmentCacheDirs() {
|
|
11056
|
-
return [attachmentCacheDir(),
|
|
11452
|
+
return [attachmentCacheDir(), join13(machineRoot(), "kanban-attachments")];
|
|
11057
11453
|
}
|
|
11058
11454
|
async function storeAttachment(id, name, body, dirs = attachmentCacheDirs()) {
|
|
11059
11455
|
const refusals = [];
|
|
11060
11456
|
for (const dir of dirs) {
|
|
11061
11457
|
try {
|
|
11062
11458
|
await mkdir14(dir, { recursive: true });
|
|
11063
|
-
const path =
|
|
11064
|
-
await
|
|
11459
|
+
const path = join13(dir, safeLocalName(id, name));
|
|
11460
|
+
await writeFile12(path, body);
|
|
11065
11461
|
void purgeStaleAttachments(dir);
|
|
11066
11462
|
return path;
|
|
11067
11463
|
} catch (err) {
|
|
@@ -11363,8 +11759,8 @@ var utilityRouter = router({
|
|
|
11363
11759
|
// ../server/src/trpc/routers/wiki.ts
|
|
11364
11760
|
import { z as z23 } from "zod";
|
|
11365
11761
|
import { TRPCError as TRPCError19 } from "@trpc/server";
|
|
11366
|
-
import { readFile as readFile13, readdir as readdir6, stat as stat4, realpath, writeFile as
|
|
11367
|
-
import { resolve as resolvePath, join as
|
|
11762
|
+
import { readFile as readFile13, readdir as readdir6, stat as stat4, realpath, writeFile as writeFile13, mkdir as mkdir15, rename as rename10, rm as rm4 } from "fs/promises";
|
|
11763
|
+
import { resolve as resolvePath, join as join14, relative, sep as sep3, dirname as dirname7 } from "path";
|
|
11368
11764
|
var SCOPE_SCHEMA = z23.union([
|
|
11369
11765
|
z23.literal("global"),
|
|
11370
11766
|
z23.string().regex(/^service:[A-Za-z0-9_-]{1,128}$/, "Invalid service scope")
|
|
@@ -11402,7 +11798,7 @@ async function resolveScope(scope) {
|
|
|
11402
11798
|
}
|
|
11403
11799
|
const dataDirAbs = resolvePath(env.DATA_DIR);
|
|
11404
11800
|
await mkdir15(dataDirAbs, { recursive: true });
|
|
11405
|
-
const base =
|
|
11801
|
+
const base = dirname7(await realpath(dataDirAbs));
|
|
11406
11802
|
const target = resolvePath(base, sub);
|
|
11407
11803
|
if (target !== base && !(target + sep3).startsWith(base + sep3)) {
|
|
11408
11804
|
throw new TRPCError19({ code: "FORBIDDEN", message: "Hors du dossier de donn\xE9es du serveur" });
|
|
@@ -11434,7 +11830,7 @@ async function walkLocal(root, dir, depth, counter) {
|
|
|
11434
11830
|
for (const e of entries) {
|
|
11435
11831
|
if (counter.n >= MAX_FILES) break;
|
|
11436
11832
|
if (e.name.startsWith(".") && e.name !== ".") continue;
|
|
11437
|
-
const abs =
|
|
11833
|
+
const abs = join14(dir, e.name);
|
|
11438
11834
|
const rel = relative(root, abs).split(sep3).join("/");
|
|
11439
11835
|
if (e.isDirectory()) {
|
|
11440
11836
|
if (IGNORED_DIRS.has(e.name)) continue;
|
|
@@ -11468,7 +11864,7 @@ async function resolveSafeLocal(root, relPath, requireExist) {
|
|
|
11468
11864
|
}
|
|
11469
11865
|
return real;
|
|
11470
11866
|
}
|
|
11471
|
-
const parent =
|
|
11867
|
+
const parent = dirname7(target);
|
|
11472
11868
|
let realParent;
|
|
11473
11869
|
try {
|
|
11474
11870
|
realParent = await realpath(parent);
|
|
@@ -11514,8 +11910,8 @@ var wikiRouter = router({
|
|
|
11514
11910
|
return agentManager.agentMutation(target.agentId, "wiki.writeMarkdown", { cwd: target.cwd, path: input.path, content: input.content });
|
|
11515
11911
|
}
|
|
11516
11912
|
const file = await resolveSafeLocal(target.root, input.path, false);
|
|
11517
|
-
await mkdir15(
|
|
11518
|
-
await
|
|
11913
|
+
await mkdir15(dirname7(file), { recursive: true });
|
|
11914
|
+
await writeFile13(file, input.content, "utf-8");
|
|
11519
11915
|
return { path: input.path, ok: true };
|
|
11520
11916
|
}),
|
|
11521
11917
|
createFile: protectedProcedure.input(z23.object({ scope: SCOPE_SCHEMA, path: REL_PATH_SCHEMA })).mutation(async ({ input }) => {
|
|
@@ -11533,8 +11929,8 @@ var wikiRouter = router({
|
|
|
11533
11929
|
} catch (err) {
|
|
11534
11930
|
if (err instanceof TRPCError19) throw err;
|
|
11535
11931
|
}
|
|
11536
|
-
await mkdir15(
|
|
11537
|
-
await
|
|
11932
|
+
await mkdir15(dirname7(file), { recursive: true });
|
|
11933
|
+
await writeFile13(file, "", "utf-8");
|
|
11538
11934
|
return { path: input.path, ok: true };
|
|
11539
11935
|
}),
|
|
11540
11936
|
createDir: protectedProcedure.input(z23.object({ scope: SCOPE_SCHEMA, path: REL_PATH_SCHEMA })).mutation(async ({ input }) => {
|
|
@@ -11559,7 +11955,7 @@ var wikiRouter = router({
|
|
|
11559
11955
|
} catch (err) {
|
|
11560
11956
|
if (err instanceof TRPCError19) throw err;
|
|
11561
11957
|
}
|
|
11562
|
-
await mkdir15(
|
|
11958
|
+
await mkdir15(dirname7(to), { recursive: true });
|
|
11563
11959
|
await rename10(from, to);
|
|
11564
11960
|
return { fromPath: input.fromPath, toPath: input.toPath, ok: true };
|
|
11565
11961
|
}),
|
|
@@ -11583,7 +11979,7 @@ var wikiRouter = router({
|
|
|
11583
11979
|
}
|
|
11584
11980
|
const fp = await resolveSafeLocal(target.root, input.path, true);
|
|
11585
11981
|
if (fp === target.root) throw new TRPCError19({ code: "FORBIDDEN", message: "Impossible de supprimer la racine" });
|
|
11586
|
-
await
|
|
11982
|
+
await rm4(fp, { recursive: true, force: true });
|
|
11587
11983
|
return { path: input.path, ok: true };
|
|
11588
11984
|
})
|
|
11589
11985
|
});
|
|
@@ -11601,31 +11997,50 @@ import { z as z24 } from "zod";
|
|
|
11601
11997
|
import { TRPCError as TRPCError20 } from "@trpc/server";
|
|
11602
11998
|
|
|
11603
11999
|
// ../server/src/services/cloud-sync-store.ts
|
|
11604
|
-
import { readFile as readFile14, writeFile as
|
|
11605
|
-
import { join as join18 } from "path";
|
|
12000
|
+
import { readFile as readFile14, writeFile as writeFile14, rename as rename11, mkdir as mkdir16, chmod as chmod9 } from "fs/promises";
|
|
11606
12001
|
var FILENAME5 = "cloud-sync.json";
|
|
11607
12002
|
var CloudSyncStore = class {
|
|
11608
12003
|
links = /* @__PURE__ */ new Map();
|
|
11609
12004
|
loaded = false;
|
|
12005
|
+
/** Machine-wide: this describes the machine, not any one project. */
|
|
11610
12006
|
getFilePath() {
|
|
11611
|
-
return
|
|
12007
|
+
return machineFilePath(FILENAME5);
|
|
12008
|
+
}
|
|
12009
|
+
/** Where older versions kept it — read only, so an older Runeya keeps working. */
|
|
12010
|
+
getLegacyFilePath() {
|
|
12011
|
+
return legacyMachineFilePath(FILENAME5);
|
|
12012
|
+
}
|
|
12013
|
+
/**
|
|
12014
|
+
* Read the machine file, falling back to the launch directory of old.
|
|
12015
|
+
*
|
|
12016
|
+
* The legacy file is left in place: the next save writes the new location,
|
|
12017
|
+
* and a directory still opened by an older Runeya keeps working meanwhile.
|
|
12018
|
+
*/
|
|
12019
|
+
async readFromDisk() {
|
|
12020
|
+
try {
|
|
12021
|
+
return await readFile14(this.getFilePath(), "utf-8");
|
|
12022
|
+
} catch {
|
|
12023
|
+
return readFile14(this.getLegacyFilePath(), "utf-8");
|
|
12024
|
+
}
|
|
11612
12025
|
}
|
|
11613
12026
|
async load() {
|
|
11614
12027
|
if (this.loaded) return;
|
|
11615
12028
|
try {
|
|
11616
|
-
const raw = await
|
|
12029
|
+
const raw = await this.readFromDisk();
|
|
11617
12030
|
const data = JSON.parse(raw);
|
|
11618
|
-
for (const l of data)
|
|
12031
|
+
for (const l of data) {
|
|
12032
|
+
this.links.set(l.orgId, { orgId: l.orgId, cloudUrl: l.cloudUrl });
|
|
12033
|
+
}
|
|
11619
12034
|
} catch {
|
|
11620
12035
|
}
|
|
11621
12036
|
this.loaded = true;
|
|
11622
12037
|
}
|
|
11623
12038
|
async save() {
|
|
11624
|
-
await mkdir16(
|
|
12039
|
+
await mkdir16(machineRoot(), { recursive: true });
|
|
11625
12040
|
const filePath = this.getFilePath();
|
|
11626
12041
|
const tmpPath = filePath + ".tmp";
|
|
11627
12042
|
const data = JSON.stringify(Array.from(this.links.values()), null, 2);
|
|
11628
|
-
await
|
|
12043
|
+
await writeFile14(tmpPath, data, "utf-8");
|
|
11629
12044
|
await rename11(tmpPath, filePath);
|
|
11630
12045
|
await chmod9(filePath, 384);
|
|
11631
12046
|
}
|
|
@@ -11653,7 +12068,82 @@ var cloudSyncStore = new CloudSyncStore();
|
|
|
11653
12068
|
|
|
11654
12069
|
// ../server/src/services/cloud-sync-bridge.ts
|
|
11655
12070
|
import { randomUUID as randomUUID11 } from "crypto";
|
|
12071
|
+
import { mkdir as mkdir18, rm as rm5 } from "fs/promises";
|
|
11656
12072
|
import * as Y from "yjs";
|
|
12073
|
+
|
|
12074
|
+
// ../server/src/services/org-lock.ts
|
|
12075
|
+
import { open as open2, readFile as readFile15, unlink as unlink4, mkdir as mkdir17 } from "fs/promises";
|
|
12076
|
+
import { readFileSync, unlinkSync } from "fs";
|
|
12077
|
+
import { join as join15 } from "path";
|
|
12078
|
+
var held = /* @__PURE__ */ new Set();
|
|
12079
|
+
function lockPath(orgId) {
|
|
12080
|
+
return join15(orgVaultRoot(orgId), "org.lock");
|
|
12081
|
+
}
|
|
12082
|
+
function isRunning(pid) {
|
|
12083
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
12084
|
+
try {
|
|
12085
|
+
process.kill(pid, 0);
|
|
12086
|
+
return true;
|
|
12087
|
+
} catch (err) {
|
|
12088
|
+
return err.code === "EPERM";
|
|
12089
|
+
}
|
|
12090
|
+
}
|
|
12091
|
+
async function readLock(path) {
|
|
12092
|
+
try {
|
|
12093
|
+
const parsed = JSON.parse(await readFile15(path, "utf-8"));
|
|
12094
|
+
return typeof parsed?.pid === "number" ? parsed : null;
|
|
12095
|
+
} catch {
|
|
12096
|
+
return null;
|
|
12097
|
+
}
|
|
12098
|
+
}
|
|
12099
|
+
async function acquireOrgLock(orgId) {
|
|
12100
|
+
const path = lockPath(orgId);
|
|
12101
|
+
await mkdir17(orgVaultRoot(orgId), { recursive: true });
|
|
12102
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
12103
|
+
try {
|
|
12104
|
+
const handle = await open2(path, "wx");
|
|
12105
|
+
try {
|
|
12106
|
+
const contents = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
12107
|
+
await handle.writeFile(JSON.stringify(contents), "utf-8");
|
|
12108
|
+
} finally {
|
|
12109
|
+
await handle.close();
|
|
12110
|
+
}
|
|
12111
|
+
held.add(orgId);
|
|
12112
|
+
return { acquired: true };
|
|
12113
|
+
} catch (err) {
|
|
12114
|
+
if (err.code !== "EEXIST") throw err;
|
|
12115
|
+
}
|
|
12116
|
+
const holder2 = await readLock(path);
|
|
12117
|
+
if (holder2 && holder2.pid !== process.pid && isRunning(holder2.pid)) {
|
|
12118
|
+
return { acquired: false, heldBy: holder2.pid };
|
|
12119
|
+
}
|
|
12120
|
+
await unlink4(path).catch(() => {
|
|
12121
|
+
});
|
|
12122
|
+
}
|
|
12123
|
+
const holder = await readLock(path);
|
|
12124
|
+
return holder ? { acquired: false, heldBy: holder.pid } : { acquired: true };
|
|
12125
|
+
}
|
|
12126
|
+
async function releaseOrgLock(orgId) {
|
|
12127
|
+
held.delete(orgId);
|
|
12128
|
+
const path = lockPath(orgId);
|
|
12129
|
+
const holder = await readLock(path);
|
|
12130
|
+
if (holder && holder.pid !== process.pid) return;
|
|
12131
|
+
await unlink4(path).catch(() => {
|
|
12132
|
+
});
|
|
12133
|
+
}
|
|
12134
|
+
function releaseAllOrgLocksSync() {
|
|
12135
|
+
for (const orgId of held) {
|
|
12136
|
+
const path = lockPath(orgId);
|
|
12137
|
+
try {
|
|
12138
|
+
const holder = JSON.parse(readFileSync(path, "utf-8"));
|
|
12139
|
+
if (holder.pid === process.pid) unlinkSync(path);
|
|
12140
|
+
} catch {
|
|
12141
|
+
}
|
|
12142
|
+
}
|
|
12143
|
+
held.clear();
|
|
12144
|
+
}
|
|
12145
|
+
|
|
12146
|
+
// ../server/src/services/cloud-sync-bridge.ts
|
|
11657
12147
|
var LOCAL_ORIGIN = "local-bridge";
|
|
11658
12148
|
var REMOTE_ORIGIN = "remote";
|
|
11659
12149
|
var RECONNECT_DELAY_MS = 3e3;
|
|
@@ -11690,6 +12180,32 @@ var OrgSyncConnection = class {
|
|
|
11690
12180
|
lastIncompatibleVersion = null;
|
|
11691
12181
|
/** Local state still owed to the cloud (initial sync, or a rejected episode). */
|
|
11692
12182
|
needsPush = false;
|
|
12183
|
+
/**
|
|
12184
|
+
* Set while a newly followed project is being materialized.
|
|
12185
|
+
*
|
|
12186
|
+
* Reconciliation reads "absent locally" as "removed from the project", which
|
|
12187
|
+
* is true for an edit and catastrophically false mid-pull: a project whose
|
|
12188
|
+
* files have not landed yet would have its services and environments wiped
|
|
12189
|
+
* from the doc — for every member of the org, not just here.
|
|
12190
|
+
*/
|
|
12191
|
+
pulling = false;
|
|
12192
|
+
/**
|
|
12193
|
+
* Projects of this org materialized on this machine — the ones whose vault
|
|
12194
|
+
* exists. Refreshed from disk rather than held in a saved list: a list is one
|
|
12195
|
+
* more thing that can disagree with what is actually there, and a project on
|
|
12196
|
+
* disk but missing from it is the "already here, still offered as a download"
|
|
12197
|
+
* bug this replaces.
|
|
12198
|
+
*/
|
|
12199
|
+
tracked = /* @__PURE__ */ new Set();
|
|
12200
|
+
/**
|
|
12201
|
+
* Whether the doc's opening state has been applied.
|
|
12202
|
+
*
|
|
12203
|
+
* It separates "the org already had these projects" from "someone just
|
|
12204
|
+
* created one". The first must not land here — joining a fifty-project org
|
|
12205
|
+
* would drop all fifty on the disk — while the second should, so a colleague
|
|
12206
|
+
* creating a project sees it appear on their teammates' machines.
|
|
12207
|
+
*/
|
|
12208
|
+
initialDocApplied = false;
|
|
11693
12209
|
unsubscribes = [];
|
|
11694
12210
|
onSaved = () => void this.reconcileLocalToDoc();
|
|
11695
12211
|
constructor(link) {
|
|
@@ -11709,6 +12225,7 @@ var OrgSyncConnection = class {
|
|
|
11709
12225
|
return this.link;
|
|
11710
12226
|
}
|
|
11711
12227
|
async start() {
|
|
12228
|
+
await this.refreshTracked();
|
|
11712
12229
|
this.doc.on("update", (update, origin) => {
|
|
11713
12230
|
if (this.stopped || this.initializing) return;
|
|
11714
12231
|
if (origin === LOCAL_ORIGIN) void this.pushUpdate(update);
|
|
@@ -11716,7 +12233,9 @@ var OrgSyncConnection = class {
|
|
|
11716
12233
|
for (const name of COLLECTIONS) {
|
|
11717
12234
|
this.collection(name).observeDeep((events, txn) => {
|
|
11718
12235
|
if (txn.origin !== REMOTE_ORIGIN) return;
|
|
11719
|
-
|
|
12236
|
+
const isInitial = !this.initialDocApplied;
|
|
12237
|
+
if (name === "projects") this.initialDocApplied = true;
|
|
12238
|
+
void this.applyDocEventsToStore(name, events, isInitial);
|
|
11720
12239
|
});
|
|
11721
12240
|
}
|
|
11722
12241
|
projectStore.events.on("saved", this.onSaved);
|
|
@@ -11903,25 +12422,152 @@ var OrgSyncConnection = class {
|
|
|
11903
12422
|
}
|
|
11904
12423
|
}
|
|
11905
12424
|
}
|
|
12425
|
+
// ---- Project selection ------------------------------------------------
|
|
12426
|
+
/**
|
|
12427
|
+
* Whether a doc entity is one this machine holds.
|
|
12428
|
+
*
|
|
12429
|
+
* The org is the unit of transport — the doc carries every project a member
|
|
12430
|
+
* can see — but not the unit of storage: a machine materializes only the
|
|
12431
|
+
* projects its user opened. Without this filter, opening one project of a
|
|
12432
|
+
* fifty-project org would drop the other forty-nine onto this disk.
|
|
12433
|
+
*
|
|
12434
|
+
* Having a vault is what "followed" means. Opening a project creates its
|
|
12435
|
+
* directory, dropping it removes the directory, and nothing else has to be
|
|
12436
|
+
* kept in agreement with the disk.
|
|
12437
|
+
*/
|
|
12438
|
+
isTracked(name, id) {
|
|
12439
|
+
if (name === "projects") return this.tracked.has(id);
|
|
12440
|
+
const fields = readEntity(this.collection(name), id);
|
|
12441
|
+
if (!fields) return false;
|
|
12442
|
+
if (name === "environments") {
|
|
12443
|
+
const projectId = fields["projectId"];
|
|
12444
|
+
return typeof projectId === "string" && this.tracked.has(projectId);
|
|
12445
|
+
}
|
|
12446
|
+
const projects = this.collection("projects");
|
|
12447
|
+
for (const projectId of this.tracked) {
|
|
12448
|
+
const project = readEntity(projects, projectId);
|
|
12449
|
+
const serviceIds = project?.["serviceIds"];
|
|
12450
|
+
if (Array.isArray(serviceIds) && serviceIds.includes(id)) return true;
|
|
12451
|
+
}
|
|
12452
|
+
return false;
|
|
12453
|
+
}
|
|
12454
|
+
/** Re-read which projects of this org have a vault on this machine. */
|
|
12455
|
+
async refreshTracked() {
|
|
12456
|
+
const vaults = await listCloudVaults();
|
|
12457
|
+
this.tracked = new Set(
|
|
12458
|
+
vaults.filter((v) => v.orgId === this.link.orgId).map((v) => v.projectId)
|
|
12459
|
+
);
|
|
12460
|
+
}
|
|
12461
|
+
/**
|
|
12462
|
+
* Whether a *removal* announced by the doc concerns this machine.
|
|
12463
|
+
*
|
|
12464
|
+
* Deletions cannot go through `isTracked`: the entity is already gone from
|
|
12465
|
+
* the doc, so there is nothing left to read its `projectId` from. Projects
|
|
12466
|
+
* still answer from the selection; for their contents we lean on
|
|
12467
|
+
* `deleteLocally` being a no-op for anything this machine never pulled, which
|
|
12468
|
+
* is precisely the untracked case.
|
|
12469
|
+
*/
|
|
12470
|
+
isTrackedForDeletion(name, id) {
|
|
12471
|
+
if (name === "projects") return this.tracked.has(id);
|
|
12472
|
+
return true;
|
|
12473
|
+
}
|
|
12474
|
+
/**
|
|
12475
|
+
* Follow a project: give it a vault, then fill it from the doc.
|
|
12476
|
+
*
|
|
12477
|
+
* Idempotent on purpose. Widening a selection is not the only way a project
|
|
12478
|
+
* comes to be missing — a first pull can fail on a rejected session, or land
|
|
12479
|
+
* before the doc arrived — and re-opening the project has to be able to retry
|
|
12480
|
+
* it. Returns whether the doc actually held the project.
|
|
12481
|
+
*/
|
|
12482
|
+
async followProject(projectId) {
|
|
12483
|
+
const inDoc = readEntity(this.collection("projects"), projectId) !== null;
|
|
12484
|
+
const existing = await projectStore.get(projectId);
|
|
12485
|
+
if (existing && !existing.orgId) {
|
|
12486
|
+
console.error(
|
|
12487
|
+
"[cloud-sync] project %s is already a local project here \u2014 not following it for org %s",
|
|
12488
|
+
projectId,
|
|
12489
|
+
this.link.orgId
|
|
12490
|
+
);
|
|
12491
|
+
return false;
|
|
12492
|
+
}
|
|
12493
|
+
const isLocal = existing?.orgId === this.link.orgId;
|
|
12494
|
+
if (!inDoc && !isLocal) return false;
|
|
12495
|
+
await mkdir18(projectVaultRoot(this.link.orgId, projectId), { recursive: true });
|
|
12496
|
+
this.tracked.add(projectId);
|
|
12497
|
+
if (inDoc) await this.pullFromDoc([projectId]);
|
|
12498
|
+
else await this.reconcileLocalToDoc();
|
|
12499
|
+
return true;
|
|
12500
|
+
}
|
|
12501
|
+
/**
|
|
12502
|
+
* Stop following a project on this machine.
|
|
12503
|
+
*
|
|
12504
|
+
* Its vault goes with it — the directory *is* the selection — but the doc is
|
|
12505
|
+
* left untouched: the user stops following the project here, they do not ask
|
|
12506
|
+
* for it to be deleted for the whole organization.
|
|
12507
|
+
*/
|
|
12508
|
+
async unfollowProject(projectId) {
|
|
12509
|
+
this.tracked.delete(projectId);
|
|
12510
|
+
await projectStore.forgetLocally(projectId);
|
|
12511
|
+
await rm5(projectVaultRoot(this.link.orgId, projectId), { recursive: true, force: true }).catch(() => {
|
|
12512
|
+
});
|
|
12513
|
+
}
|
|
12514
|
+
/**
|
|
12515
|
+
* Materialize locally everything the doc holds for these projects.
|
|
12516
|
+
*
|
|
12517
|
+
* Contents land before their project, so that the moment the project becomes
|
|
12518
|
+
* local — and therefore "owned" by reconciliation — its services and
|
|
12519
|
+
* environments are already on disk. The guard covers the rest of the window,
|
|
12520
|
+
* including the saves this very method triggers.
|
|
12521
|
+
*/
|
|
12522
|
+
async pullFromDoc(projectIds) {
|
|
12523
|
+
this.pulling = true;
|
|
12524
|
+
try {
|
|
12525
|
+
for (const name of ["services", "environments", "projects"]) {
|
|
12526
|
+
const collection = this.collection(name);
|
|
12527
|
+
for (const id of Array.from(collection.keys())) {
|
|
12528
|
+
if (name === "projects" ? !projectIds.includes(id) : !this.isTracked(name, id)) continue;
|
|
12529
|
+
const fields = readEntity(collection, id);
|
|
12530
|
+
if (fields) await this.upsertLocally(name, id, fields);
|
|
12531
|
+
}
|
|
12532
|
+
}
|
|
12533
|
+
} finally {
|
|
12534
|
+
this.pulling = false;
|
|
12535
|
+
}
|
|
12536
|
+
await this.reconcileLocalToDoc();
|
|
12537
|
+
}
|
|
11906
12538
|
// ---- Local ↔ Doc reconciliation --------------------------------------
|
|
11907
|
-
async applyDocEventsToStore(name, events) {
|
|
12539
|
+
async applyDocEventsToStore(name, events, isInitial = false) {
|
|
11908
12540
|
const collection = this.collection(name);
|
|
11909
12541
|
const deleted = /* @__PURE__ */ new Set();
|
|
11910
12542
|
const affected = /* @__PURE__ */ new Set();
|
|
12543
|
+
const added = /* @__PURE__ */ new Set();
|
|
11911
12544
|
for (const event of events) {
|
|
11912
12545
|
if (event.target === collection) {
|
|
11913
12546
|
for (const [key, change] of event.changes.keys) {
|
|
11914
12547
|
if (change.action === "delete") deleted.add(key);
|
|
11915
|
-
else
|
|
12548
|
+
else {
|
|
12549
|
+
affected.add(key);
|
|
12550
|
+
if (change.action === "add") added.add(key);
|
|
12551
|
+
}
|
|
11916
12552
|
}
|
|
11917
12553
|
} else {
|
|
11918
12554
|
const topKey = event.path[0];
|
|
11919
12555
|
if (typeof topKey === "string") affected.add(topKey);
|
|
11920
12556
|
}
|
|
11921
12557
|
}
|
|
11922
|
-
for (const id of deleted)
|
|
12558
|
+
for (const id of deleted) {
|
|
12559
|
+
if (!this.isTrackedForDeletion(name, id)) continue;
|
|
12560
|
+
await this.deleteLocally(name, id);
|
|
12561
|
+
}
|
|
12562
|
+
if (name === "projects" && !isInitial) {
|
|
12563
|
+
for (const id of added) {
|
|
12564
|
+
if (deleted.has(id) || this.tracked.has(id)) continue;
|
|
12565
|
+
await this.followProject(id);
|
|
12566
|
+
}
|
|
12567
|
+
}
|
|
11923
12568
|
for (const id of affected) {
|
|
11924
12569
|
if (deleted.has(id)) continue;
|
|
12570
|
+
if (!this.isTracked(name, id)) continue;
|
|
11925
12571
|
const fields = readEntity(collection, id);
|
|
11926
12572
|
if (!fields) continue;
|
|
11927
12573
|
await this.upsertLocally(name, id, fields);
|
|
@@ -11936,7 +12582,10 @@ var OrgSyncConnection = class {
|
|
|
11936
12582
|
await environmentStore.applyRemoteUpsert(id, fields);
|
|
11937
12583
|
return;
|
|
11938
12584
|
}
|
|
11939
|
-
const result = await projectStore.applyRemoteUpsert(id,
|
|
12585
|
+
const result = await projectStore.applyRemoteUpsert(id, {
|
|
12586
|
+
...fields,
|
|
12587
|
+
orgId: this.link.orgId
|
|
12588
|
+
});
|
|
11940
12589
|
if (result?.created) await this.bootstrapEnvironment(id);
|
|
11941
12590
|
}
|
|
11942
12591
|
async deleteLocally(name, id) {
|
|
@@ -11977,7 +12626,7 @@ var OrgSyncConnection = class {
|
|
|
11977
12626
|
* propagate — otherwise a service could never be removed.
|
|
11978
12627
|
*/
|
|
11979
12628
|
async reconcileLocalToDoc() {
|
|
11980
|
-
if (this.stopped) return;
|
|
12629
|
+
if (this.stopped || this.pulling) return;
|
|
11981
12630
|
const projects = await projectStore.listDiskFormByOrg(this.link.orgId);
|
|
11982
12631
|
const projectIds = projects.map((p) => p.id);
|
|
11983
12632
|
const serviceIds = [...new Set(projects.flatMap((p) => p.serviceIds ?? []))];
|
|
@@ -12025,6 +12674,8 @@ var OrgSyncConnection = class {
|
|
|
12025
12674
|
};
|
|
12026
12675
|
var CloudSyncBridge = class {
|
|
12027
12676
|
connections = /* @__PURE__ */ new Map();
|
|
12677
|
+
/** Orgs another instance holds the lock on — reported, not retried blindly. */
|
|
12678
|
+
locked = /* @__PURE__ */ new Set();
|
|
12028
12679
|
started = false;
|
|
12029
12680
|
/** Start syncing every organization already linked on disk. */
|
|
12030
12681
|
async start() {
|
|
@@ -12050,15 +12701,55 @@ var CloudSyncBridge = class {
|
|
|
12050
12701
|
}
|
|
12051
12702
|
/** Per-org sync state, for the UI. */
|
|
12052
12703
|
states() {
|
|
12053
|
-
|
|
12704
|
+
const live = Array.from(this.connections.entries()).map(([orgId, conn]) => ({
|
|
12054
12705
|
orgId,
|
|
12055
12706
|
state: conn.getState(),
|
|
12056
12707
|
lastIncompatibleVersion: conn.getLastIncompatibleVersion()
|
|
12057
12708
|
}));
|
|
12709
|
+
const locked = Array.from(this.locked).filter((orgId) => !this.connections.has(orgId)).map((orgId) => ({ orgId, state: "locked", lastIncompatibleVersion: null }));
|
|
12710
|
+
return [...live, ...locked];
|
|
12058
12711
|
}
|
|
12059
|
-
/**
|
|
12712
|
+
/**
|
|
12713
|
+
* Follow one project of an org on this machine, pulling it now.
|
|
12714
|
+
*
|
|
12715
|
+
* Returns false when the org is not connected or the doc does not hold the
|
|
12716
|
+
* project — both mean "not on this machine yet", and the caller reports the
|
|
12717
|
+
* sync state rather than a bare timeout.
|
|
12718
|
+
*/
|
|
12719
|
+
async followProject(orgId, projectId) {
|
|
12720
|
+
const conn = this.connections.get(orgId);
|
|
12721
|
+
if (!conn) return false;
|
|
12722
|
+
return conn.followProject(projectId);
|
|
12723
|
+
}
|
|
12724
|
+
/** Stop following one project of an org on this machine. */
|
|
12725
|
+
async unfollowProject(orgId, projectId) {
|
|
12726
|
+
await this.connections.get(orgId)?.unfollowProject(projectId);
|
|
12727
|
+
}
|
|
12728
|
+
/**
|
|
12729
|
+
* Begin (or restart) syncing a single organization.
|
|
12730
|
+
*
|
|
12731
|
+
* Takes the org's lock first. An org's vaults are shared by every Runeya on
|
|
12732
|
+
* this machine, so a second instance syncing the same org would write the
|
|
12733
|
+
* same files from its own copy of the doc — silently overwriting the first.
|
|
12734
|
+
* Refusing to sync is the safe answer: the instance keeps running, that org
|
|
12735
|
+
* simply does not sync here.
|
|
12736
|
+
*/
|
|
12060
12737
|
async connect(link) {
|
|
12061
12738
|
this.disconnect(link.orgId);
|
|
12739
|
+
const lock = await acquireOrgLock(link.orgId).catch((err) => {
|
|
12740
|
+
console.error("[cloud-sync] could not take the lock for org %s:", link.orgId, err.message);
|
|
12741
|
+
return { acquired: false, heldBy: 0 };
|
|
12742
|
+
});
|
|
12743
|
+
if (!lock.acquired) {
|
|
12744
|
+
console.error(
|
|
12745
|
+
"[cloud-sync] org %s is already being synced by another Runeya (pid %d) \u2014 not syncing it here",
|
|
12746
|
+
link.orgId,
|
|
12747
|
+
lock.heldBy
|
|
12748
|
+
);
|
|
12749
|
+
this.locked.add(link.orgId);
|
|
12750
|
+
return;
|
|
12751
|
+
}
|
|
12752
|
+
this.locked.delete(link.orgId);
|
|
12062
12753
|
const conn = new OrgSyncConnection(link);
|
|
12063
12754
|
this.connections.set(link.orgId, conn);
|
|
12064
12755
|
try {
|
|
@@ -12072,19 +12763,22 @@ var CloudSyncBridge = class {
|
|
|
12072
12763
|
if (existing) {
|
|
12073
12764
|
existing.stop();
|
|
12074
12765
|
this.connections.delete(orgId);
|
|
12766
|
+
void releaseOrgLock(orgId);
|
|
12075
12767
|
}
|
|
12076
12768
|
}
|
|
12077
12769
|
stopAll() {
|
|
12078
12770
|
for (const conn of this.connections.values()) conn.stop();
|
|
12079
12771
|
this.connections.clear();
|
|
12772
|
+
this.locked.clear();
|
|
12773
|
+
releaseAllOrgLocksSync();
|
|
12080
12774
|
}
|
|
12081
12775
|
};
|
|
12082
12776
|
var cloudSyncBridge = new CloudSyncBridge();
|
|
12083
12777
|
|
|
12084
12778
|
// ../server/src/services/master-password.ts
|
|
12085
12779
|
import { randomBytes as randomBytes4, createCipheriv, createDecipheriv } from "crypto";
|
|
12086
|
-
import { readFile as
|
|
12087
|
-
import { join as
|
|
12780
|
+
import { readFile as readFile16, writeFile as writeFile15, chmod as chmod10, stat as stat5, rename as rename12 } from "fs/promises";
|
|
12781
|
+
import { join as join16 } from "path";
|
|
12088
12782
|
import { Entry } from "@napi-rs/keyring";
|
|
12089
12783
|
import { hashRaw as argon2HashRaw } from "@node-rs/argon2";
|
|
12090
12784
|
var KEYRING_SERVICE = "runeya";
|
|
@@ -12098,7 +12792,7 @@ var ARGON2_OPTIONS = { memoryCost: 65536, timeCost: 3, parallelism: 4, outputLen
|
|
|
12098
12792
|
async function readKeyFile(filePath) {
|
|
12099
12793
|
const s = await stat5(filePath);
|
|
12100
12794
|
if (s.size > MAX_KEY_FILE_SIZE) throw new Error(`Fichier suspect : ${filePath} (${s.size} octets)`);
|
|
12101
|
-
return
|
|
12795
|
+
return readFile16(filePath);
|
|
12102
12796
|
}
|
|
12103
12797
|
function aesEncrypt(key, plaintext) {
|
|
12104
12798
|
const iv = randomBytes4(12);
|
|
@@ -12117,7 +12811,7 @@ function aesDecrypt(key, data) {
|
|
|
12117
12811
|
}
|
|
12118
12812
|
async function getPepperFromFile(dataDir) {
|
|
12119
12813
|
try {
|
|
12120
|
-
const raw = await readKeyFile(
|
|
12814
|
+
const raw = await readKeyFile(join16(dataDir, PEPPER_FILENAME));
|
|
12121
12815
|
const hex = raw.toString("utf-8").trim();
|
|
12122
12816
|
if (/^[0-9a-f]{64}$/i.test(hex)) return Buffer.from(hex, "hex");
|
|
12123
12817
|
} catch {
|
|
@@ -12125,9 +12819,9 @@ async function getPepperFromFile(dataDir) {
|
|
|
12125
12819
|
return null;
|
|
12126
12820
|
}
|
|
12127
12821
|
async function writePepperToFile(dataDir, pepper) {
|
|
12128
|
-
const filePath =
|
|
12822
|
+
const filePath = join16(dataDir, PEPPER_FILENAME);
|
|
12129
12823
|
const tmpPath = filePath + ".tmp";
|
|
12130
|
-
await
|
|
12824
|
+
await writeFile15(tmpPath, pepper.toString("hex"), "utf-8");
|
|
12131
12825
|
await chmod10(tmpPath, 384);
|
|
12132
12826
|
await rename12(tmpPath, filePath);
|
|
12133
12827
|
}
|
|
@@ -12176,14 +12870,14 @@ async function writeEncryptedKey(dataDir, masterPassword, encryptionKey) {
|
|
|
12176
12870
|
const payload = aesEncrypt(derivedKey, encryptionKey);
|
|
12177
12871
|
const fileContent = Buffer.concat([salt, payload]);
|
|
12178
12872
|
const encoded = fileContent.toString("base64");
|
|
12179
|
-
const filePath =
|
|
12873
|
+
const filePath = join16(dataDir, ENCRYPTED_KEY_FILENAME);
|
|
12180
12874
|
const tmpPath = filePath + ".tmp";
|
|
12181
|
-
await
|
|
12875
|
+
await writeFile15(tmpPath, encoded, "utf-8");
|
|
12182
12876
|
await chmod10(tmpPath, 384);
|
|
12183
12877
|
await rename12(tmpPath, filePath);
|
|
12184
12878
|
}
|
|
12185
12879
|
async function readEncryptedKey(dataDir, masterPassword) {
|
|
12186
|
-
const filePath =
|
|
12880
|
+
const filePath = join16(dataDir, ENCRYPTED_KEY_FILENAME);
|
|
12187
12881
|
const raw = await readKeyFile(filePath);
|
|
12188
12882
|
const fileContent = Buffer.from(raw.toString("utf-8").trim(), "base64");
|
|
12189
12883
|
const salt = fileContent.subarray(0, 16);
|
|
@@ -12203,13 +12897,13 @@ async function getPepperFromKeychain(dataDir) {
|
|
|
12203
12897
|
}
|
|
12204
12898
|
async function writeVerifyFile(dataDir, encryptionKey) {
|
|
12205
12899
|
const payload = aesEncrypt(encryptionKey, Buffer.from(CANARY_PLAINTEXT, "utf-8"));
|
|
12206
|
-
const filePath =
|
|
12900
|
+
const filePath = join16(dataDir, VERIFY_FILENAME);
|
|
12207
12901
|
const tmpPath = filePath + ".tmp";
|
|
12208
|
-
await
|
|
12902
|
+
await writeFile15(tmpPath, payload.toString("base64"), "utf-8");
|
|
12209
12903
|
await rename12(tmpPath, filePath);
|
|
12210
12904
|
}
|
|
12211
12905
|
async function verifyEncryptionKey(dataDir, encryptionKey) {
|
|
12212
|
-
const filePath =
|
|
12906
|
+
const filePath = join16(dataDir, VERIFY_FILENAME);
|
|
12213
12907
|
const raw = await readKeyFile(filePath);
|
|
12214
12908
|
if (!matchesCanary(raw.toString("utf-8"), encryptionKey)) {
|
|
12215
12909
|
throw new Error("Cl\xE9 d'encryption incorrecte");
|
|
@@ -12225,14 +12919,14 @@ function matchesCanary(canaryBase64, encryptionKey) {
|
|
|
12225
12919
|
}
|
|
12226
12920
|
async function readVerifyFile(dataDir) {
|
|
12227
12921
|
try {
|
|
12228
|
-
return (await readKeyFile(
|
|
12922
|
+
return (await readKeyFile(join16(dataDir, VERIFY_FILENAME))).toString("utf-8").trim();
|
|
12229
12923
|
} catch {
|
|
12230
12924
|
return null;
|
|
12231
12925
|
}
|
|
12232
12926
|
}
|
|
12233
12927
|
async function hasVerifyFile(dataDir) {
|
|
12234
12928
|
try {
|
|
12235
|
-
await stat5(
|
|
12929
|
+
await stat5(join16(dataDir, VERIFY_FILENAME));
|
|
12236
12930
|
return true;
|
|
12237
12931
|
} catch {
|
|
12238
12932
|
return false;
|
|
@@ -12240,7 +12934,7 @@ async function hasVerifyFile(dataDir) {
|
|
|
12240
12934
|
}
|
|
12241
12935
|
async function hasEncryptedKeyFile(dataDir) {
|
|
12242
12936
|
try {
|
|
12243
|
-
await stat5(
|
|
12937
|
+
await stat5(join16(dataDir, ENCRYPTED_KEY_FILENAME));
|
|
12244
12938
|
return true;
|
|
12245
12939
|
} catch {
|
|
12246
12940
|
return false;
|
|
@@ -12253,7 +12947,7 @@ async function reconcileOrgCanary(orgId) {
|
|
|
12253
12947
|
if (!key) {
|
|
12254
12948
|
throw new TRPCError20({ code: "PRECONDITION_FAILED", message: "Encryption key not loaded." });
|
|
12255
12949
|
}
|
|
12256
|
-
const local = await readVerifyFile(
|
|
12950
|
+
const local = await readVerifyFile(await keyRoot());
|
|
12257
12951
|
if (!local) {
|
|
12258
12952
|
throw new TRPCError20({ code: "PRECONDITION_FAILED", message: "This machine has no key canary yet." });
|
|
12259
12953
|
}
|
|
@@ -12275,6 +12969,9 @@ async function reconcileOrgCanary(orgId) {
|
|
|
12275
12969
|
});
|
|
12276
12970
|
}
|
|
12277
12971
|
}
|
|
12972
|
+
function syncState(orgId) {
|
|
12973
|
+
return cloudSyncBridge.states().find((s) => s.orgId === orgId)?.state ?? "stopped";
|
|
12974
|
+
}
|
|
12278
12975
|
var cloudSyncRouter = router({
|
|
12279
12976
|
/**
|
|
12280
12977
|
* Register/replace a sync link for an org and start syncing immediately.
|
|
@@ -12285,12 +12982,56 @@ var cloudSyncRouter = router({
|
|
|
12285
12982
|
link: protectedProcedure.input(
|
|
12286
12983
|
z24.object({
|
|
12287
12984
|
orgId: z24.string().min(1).max(128),
|
|
12288
|
-
cloudUrl: z24.string().url().max(2048)
|
|
12985
|
+
cloudUrl: z24.string().url().max(2048),
|
|
12986
|
+
// No project selection: which projects this machine materializes is
|
|
12987
|
+
// read from the vaults on disk, so linking an org never re-downloads
|
|
12988
|
+
// projects the user had dropped.
|
|
12989
|
+
projectIds: z24.array(z24.string().min(1).max(128)).max(500).optional()
|
|
12289
12990
|
})
|
|
12290
12991
|
).mutation(async ({ input }) => {
|
|
12291
12992
|
await reconcileOrgCanary(input.orgId);
|
|
12292
12993
|
await cloudSyncStore.upsert(input.orgId, input.cloudUrl);
|
|
12293
|
-
await
|
|
12994
|
+
const link = await cloudSyncStore.get(input.orgId);
|
|
12995
|
+
await cloudSyncBridge.connect(link ?? { orgId: input.orgId, cloudUrl: input.cloudUrl });
|
|
12996
|
+
return { success: true };
|
|
12997
|
+
}),
|
|
12998
|
+
/**
|
|
12999
|
+
* Follow one more project of an org, and pull it now.
|
|
13000
|
+
*
|
|
13001
|
+
* This is what opening a project in the switcher calls. It links the org on
|
|
13002
|
+
* first use, so the UI never has to know whether a link already exists —
|
|
13003
|
+
* "open this project" is the whole gesture.
|
|
13004
|
+
*/
|
|
13005
|
+
trackProject: protectedProcedure.input(
|
|
13006
|
+
z24.object({
|
|
13007
|
+
orgId: z24.string().min(1).max(128),
|
|
13008
|
+
cloudUrl: z24.string().url().max(2048),
|
|
13009
|
+
projectId: z24.string().min(1).max(128)
|
|
13010
|
+
})
|
|
13011
|
+
).mutation(async ({ input }) => {
|
|
13012
|
+
const existing = await cloudSyncStore.get(input.orgId);
|
|
13013
|
+
if (!existing) {
|
|
13014
|
+
await reconcileOrgCanary(input.orgId);
|
|
13015
|
+
await cloudSyncStore.upsert(input.orgId, input.cloudUrl);
|
|
13016
|
+
const link = await cloudSyncStore.get(input.orgId);
|
|
13017
|
+
if (link) await cloudSyncBridge.connect(link);
|
|
13018
|
+
}
|
|
13019
|
+
const pulled = await cloudSyncBridge.followProject(input.orgId, input.projectId);
|
|
13020
|
+
return { pulled, state: syncState(input.orgId) };
|
|
13021
|
+
}),
|
|
13022
|
+
/**
|
|
13023
|
+
* Stop materializing one project of an org on this machine.
|
|
13024
|
+
*
|
|
13025
|
+
* Removes its vault, and nothing else: the project stays in the org, for
|
|
13026
|
+
* every other member and for this one on another machine.
|
|
13027
|
+
*/
|
|
13028
|
+
untrackProject: protectedProcedure.input(
|
|
13029
|
+
z24.object({
|
|
13030
|
+
orgId: z24.string().min(1).max(128),
|
|
13031
|
+
projectId: z24.string().min(1).max(128)
|
|
13032
|
+
})
|
|
13033
|
+
).mutation(async ({ input }) => {
|
|
13034
|
+
await cloudSyncBridge.unfollowProject(input.orgId, input.projectId);
|
|
12294
13035
|
return { success: true };
|
|
12295
13036
|
}),
|
|
12296
13037
|
/** Stop syncing an org and forget its token. */
|
|
@@ -12378,7 +13119,6 @@ var migrationEngine = new MigrationEngine();
|
|
|
12378
13119
|
|
|
12379
13120
|
// ../server/src/services/file-security.ts
|
|
12380
13121
|
import { chmod as chmod11, stat as stat6 } from "fs/promises";
|
|
12381
|
-
import { join as join20 } from "path";
|
|
12382
13122
|
var SENSITIVE_FILES = [
|
|
12383
13123
|
"agents.json",
|
|
12384
13124
|
"settings.json",
|
|
@@ -12390,78 +13130,36 @@ var SENSITIVE_FILES = [
|
|
|
12390
13130
|
var SECURE_MODE = 384;
|
|
12391
13131
|
async function ensureFilePermissions() {
|
|
12392
13132
|
for (const filename of SENSITIVE_FILES) {
|
|
12393
|
-
const filepath
|
|
12394
|
-
|
|
12395
|
-
|
|
12396
|
-
|
|
12397
|
-
|
|
12398
|
-
|
|
12399
|
-
|
|
12400
|
-
|
|
12401
|
-
|
|
12402
|
-
|
|
12403
|
-
|
|
13133
|
+
for (const filepath of [machineFilePath(filename), legacyMachineFilePath(filename)]) {
|
|
13134
|
+
try {
|
|
13135
|
+
const stats = await stat6(filepath);
|
|
13136
|
+
const mode = stats.mode & 511;
|
|
13137
|
+
if (mode !== SECURE_MODE) {
|
|
13138
|
+
console.warn("[security] File", filepath, "has mode", mode.toString(8) + ", fixing to 600");
|
|
13139
|
+
await chmod11(filepath, SECURE_MODE);
|
|
13140
|
+
}
|
|
13141
|
+
} catch (err) {
|
|
13142
|
+
if (err.code !== "ENOENT") {
|
|
13143
|
+
console.warn("[security] Cannot check permissions for", filepath + ":", err instanceof Error ? err.message : String(err));
|
|
13144
|
+
}
|
|
12404
13145
|
}
|
|
12405
13146
|
}
|
|
12406
13147
|
}
|
|
12407
13148
|
}
|
|
12408
13149
|
|
|
12409
|
-
// ../server/src/utils/ensure-data-gitignore.ts
|
|
12410
|
-
import { writeFile as writeFile15 } from "fs/promises";
|
|
12411
|
-
import { join as join21 } from "path";
|
|
12412
|
-
var GITIGNORE_CONTENT = `
|
|
12413
|
-
#################
|
|
12414
|
-
# Do not edit this file #
|
|
12415
|
-
#################
|
|
12416
|
-
|
|
12417
|
-
|
|
12418
|
-
# Encryption key (contains secrets \u2014 never commit)
|
|
12419
|
-
.encryption.key
|
|
12420
|
-
.encryption.key.enc
|
|
12421
|
-
|
|
12422
|
-
|
|
12423
|
-
# Key verification canary \u2014 MUST be committed so new team members can validate their encryption key
|
|
12424
|
-
!.runeya-verify
|
|
12425
|
-
|
|
12426
|
-
# This machine's cloud session token (encrypted, and specific to this install)
|
|
12427
|
-
cloud-session.json
|
|
12428
|
-
|
|
12429
|
-
# Backup in local only
|
|
12430
|
-
backup-*
|
|
12431
|
-
|
|
12432
|
-
# Per-user environment overrides (never commit \u2014 each dev has their own)
|
|
12433
|
-
environment-overrides.json
|
|
12434
|
-
|
|
12435
|
-
# Private user data (conversations, personal state \u2014 never commit)
|
|
12436
|
-
*.private
|
|
12437
|
-
*.private.json*
|
|
12438
|
-
conversations/
|
|
12439
|
-
conversations.private.json
|
|
12440
|
-
|
|
12441
|
-
|
|
12442
|
-
|
|
12443
|
-
# Runtime logs
|
|
12444
|
-
*.log
|
|
12445
|
-
*.log.*
|
|
12446
|
-
`;
|
|
12447
|
-
async function ensureDataGitignore(dataDir) {
|
|
12448
|
-
const gitignorePath = join21(dataDir, ".gitignore");
|
|
12449
|
-
await writeFile15(gitignorePath, GITIGNORE_CONTENT, "utf-8");
|
|
12450
|
-
}
|
|
12451
|
-
|
|
12452
13150
|
// ../server/src/migrations/migrate-from-legacy.ts
|
|
12453
13151
|
import { createRequire } from "module";
|
|
12454
13152
|
import { randomBytes as randomBytes5, createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2 } from "crypto";
|
|
12455
13153
|
import { randomUUID as randomUUID12 } from "crypto";
|
|
12456
13154
|
import {
|
|
12457
|
-
readFile as
|
|
13155
|
+
readFile as readFile17,
|
|
12458
13156
|
writeFile as writeFile16,
|
|
12459
13157
|
rename as rename13,
|
|
12460
|
-
mkdir as
|
|
13158
|
+
mkdir as mkdir19,
|
|
12461
13159
|
access as access2,
|
|
12462
13160
|
readdir as readdir7
|
|
12463
13161
|
} from "fs/promises";
|
|
12464
|
-
import { join as
|
|
13162
|
+
import { join as join17 } from "path";
|
|
12465
13163
|
var _require = createRequire(import.meta.url);
|
|
12466
13164
|
var PARSER_ID_MAP = {
|
|
12467
13165
|
"stack-monitor-parser-jsons": "native:json",
|
|
@@ -12570,7 +13268,7 @@ async function getSodium() {
|
|
|
12570
13268
|
return _sodium;
|
|
12571
13269
|
}
|
|
12572
13270
|
async function decryptLegacyFile(filePath, key, sodium) {
|
|
12573
|
-
const raw = (await
|
|
13271
|
+
const raw = (await readFile17(filePath, "utf-8")).trim();
|
|
12574
13272
|
if (!raw || raw === "[]" || raw === "{}") {
|
|
12575
13273
|
try {
|
|
12576
13274
|
return JSON.parse(raw);
|
|
@@ -12596,8 +13294,8 @@ async function atomicWrite(path, data) {
|
|
|
12596
13294
|
async function runLegacyMigration(dataDir) {
|
|
12597
13295
|
const { resolve: resolve6 } = await import("path");
|
|
12598
13296
|
const abCwd = resolve6(process.cwd());
|
|
12599
|
-
const legacyRoot =
|
|
12600
|
-
const legacyKeyPath =
|
|
13297
|
+
const legacyRoot = join17(abCwd, env.DATA_DIR);
|
|
13298
|
+
const legacyKeyPath = join17(legacyRoot, "dbs", "encryption-key.json");
|
|
12601
13299
|
try {
|
|
12602
13300
|
await access2(legacyKeyPath);
|
|
12603
13301
|
} catch {
|
|
@@ -12605,20 +13303,20 @@ async function runLegacyMigration(dataDir) {
|
|
|
12605
13303
|
}
|
|
12606
13304
|
console.log("[legacy-migration] Legacy Runeya data detected, starting migration\u2026");
|
|
12607
13305
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").substring(0, 19);
|
|
12608
|
-
const legacyTmp =
|
|
13306
|
+
const legacyTmp = join17(abCwd, ".runeya-tmp");
|
|
12609
13307
|
await rename13(legacyRoot, legacyTmp);
|
|
12610
|
-
await
|
|
12611
|
-
const legacyOld =
|
|
13308
|
+
await mkdir19(dataDir, { recursive: true });
|
|
13309
|
+
const legacyOld = join17(dataDir, `backup-${timestamp}`);
|
|
12612
13310
|
await rename13(legacyTmp, legacyOld);
|
|
12613
13311
|
console.log(`[legacy-migration] Legacy data backed up to ${legacyOld}`);
|
|
12614
|
-
const keyJson = JSON.parse(await
|
|
13312
|
+
const keyJson = JSON.parse(await readFile17(join17(legacyOld, "dbs", "encryption-key.json"), "utf-8"));
|
|
12615
13313
|
const sodium = await getSodium();
|
|
12616
13314
|
const sodiumKey = sodium.from_base64(keyJson.encryptionKey);
|
|
12617
13315
|
const keyBuffer = Buffer.from(sodiumKey);
|
|
12618
13316
|
const runyeaKeyHex = keyBuffer.toString("hex");
|
|
12619
13317
|
await setEncryptionKey(runyeaKeyHex);
|
|
12620
13318
|
console.log("[legacy-migration] Encryption key loaded in memory (temporary)");
|
|
12621
|
-
const dbsDir =
|
|
13319
|
+
const dbsDir = join17(legacyOld, "dbs");
|
|
12622
13320
|
async function readEncrypted(filePath) {
|
|
12623
13321
|
try {
|
|
12624
13322
|
return await decryptLegacyFile(filePath, sodiumKey, sodium);
|
|
@@ -12627,29 +13325,29 @@ async function runLegacyMigration(dataDir) {
|
|
|
12627
13325
|
return null;
|
|
12628
13326
|
}
|
|
12629
13327
|
}
|
|
12630
|
-
const envsDir =
|
|
13328
|
+
const envsDir = join17(dbsDir, "envs");
|
|
12631
13329
|
const envFiles = (await readdir7(envsDir).catch(() => [])).filter((f) => f.endsWith(".encrypted.json"));
|
|
12632
13330
|
const legacyEnvs = {};
|
|
12633
13331
|
for (const file of envFiles) {
|
|
12634
13332
|
const name = file.replace(".encrypted.json", "");
|
|
12635
|
-
legacyEnvs[name] = await readEncrypted(
|
|
13333
|
+
legacyEnvs[name] = await readEncrypted(join17(envsDir, file));
|
|
12636
13334
|
}
|
|
12637
|
-
const servicesDir =
|
|
13335
|
+
const servicesDir = join17(dbsDir, "services");
|
|
12638
13336
|
const serviceFiles = (await readdir7(servicesDir).catch(() => [])).filter((f) => f.endsWith(".encrypted.json"));
|
|
12639
13337
|
const legacyServices = {};
|
|
12640
13338
|
for (const file of serviceFiles) {
|
|
12641
13339
|
const name = file.replace(".encrypted.json", "");
|
|
12642
13340
|
if (!name) continue;
|
|
12643
|
-
const data = await readEncrypted(
|
|
13341
|
+
const data = await readEncrypted(join17(servicesDir, file));
|
|
12644
13342
|
if (data?.label) legacyServices[name] = data;
|
|
12645
13343
|
}
|
|
12646
|
-
const overridesDir =
|
|
13344
|
+
const overridesDir = join17(dbsDir, "overrides");
|
|
12647
13345
|
const overrideFiles = (await readdir7(overridesDir).catch(() => [])).filter((f) => f.endsWith(".encrypted.json"));
|
|
12648
13346
|
const legacyServiceOverrides = {};
|
|
12649
13347
|
const legacyEnvOverrides = {};
|
|
12650
13348
|
for (const file of overrideFiles) {
|
|
12651
13349
|
const baseName = file.replace(".encrypted.json", "");
|
|
12652
|
-
const data = await readEncrypted(
|
|
13350
|
+
const data = await readEncrypted(join17(overridesDir, file));
|
|
12653
13351
|
if (!data) continue;
|
|
12654
13352
|
if (baseName.endsWith("-envs")) {
|
|
12655
13353
|
legacyServiceOverrides[baseName.slice(0, -5)] = data;
|
|
@@ -12657,11 +13355,11 @@ async function runLegacyMigration(dataDir) {
|
|
|
12657
13355
|
legacyEnvOverrides[baseName.slice(0, -12)] = data;
|
|
12658
13356
|
}
|
|
12659
13357
|
}
|
|
12660
|
-
const parsersDir =
|
|
13358
|
+
const parsersDir = join17(dbsDir, "parsers");
|
|
12661
13359
|
const parserFiles = (await readdir7(parsersDir).catch(() => [])).filter((f) => f.endsWith(".encrypted.json"));
|
|
12662
13360
|
const legacyParsers = [];
|
|
12663
13361
|
for (const file of parserFiles) {
|
|
12664
|
-
const data = await readEncrypted(
|
|
13362
|
+
const data = await readEncrypted(join17(parsersDir, file));
|
|
12665
13363
|
if (data?.id) legacyParsers.push(data);
|
|
12666
13364
|
}
|
|
12667
13365
|
const projectMonoId = randomUUID12();
|
|
@@ -13002,7 +13700,7 @@ async function runLegacyMigration(dataDir) {
|
|
|
13002
13700
|
}
|
|
13003
13701
|
let existingSettings = {};
|
|
13004
13702
|
try {
|
|
13005
|
-
existingSettings = JSON.parse(await
|
|
13703
|
+
existingSettings = JSON.parse(await readFile17(join17(dataDir, "settings.json"), "utf-8"));
|
|
13006
13704
|
} catch {
|
|
13007
13705
|
}
|
|
13008
13706
|
const migratedParsers = legacyParsers.map((p) => ({
|
|
@@ -13023,11 +13721,11 @@ ${p.transform ?? ""}`,
|
|
|
13023
13721
|
...migratedParsers
|
|
13024
13722
|
]
|
|
13025
13723
|
};
|
|
13026
|
-
await atomicWrite(
|
|
13027
|
-
await atomicWrite(
|
|
13028
|
-
await atomicWrite(
|
|
13029
|
-
await atomicWrite(
|
|
13030
|
-
await atomicWrite(
|
|
13724
|
+
await atomicWrite(join17(dataDir, "projects.json"), runeProjects);
|
|
13725
|
+
await atomicWrite(join17(dataDir, "services.json"), runeServices);
|
|
13726
|
+
await atomicWrite(join17(dataDir, "environments.json"), runeEnvs);
|
|
13727
|
+
await atomicWrite(join17(dataDir, "environment-overrides.json"), flatOverrides);
|
|
13728
|
+
await atomicWrite(join17(dataDir, "settings.json"), newSettings);
|
|
13031
13729
|
const svcEnvCount = runeEnvs.filter((e) => e.scope === "service").length;
|
|
13032
13730
|
console.log(`[legacy-migration] Written: ${runeProjects.length} projects, ${runeServices.length} services, ${runeEnvs.length} environments (${svcEnvCount} service-scoped)`);
|
|
13033
13731
|
console.log(`[legacy-migration] Overrides: ${Object.keys(flatOverrides).length}, custom parsers: ${migratedParsers.length} (all disabled \u2014 rewrite required)`);
|
|
@@ -13041,9 +13739,9 @@ async function findLegacyKeyInBackup(dataDir) {
|
|
|
13041
13739
|
const entries = await readdir7(dataDir);
|
|
13042
13740
|
const backupDirs = entries.filter((e) => e.startsWith("backup-")).sort((a, b) => a < b ? -1 : a > b ? 1 : 0).reverse();
|
|
13043
13741
|
for (const dir of backupDirs) {
|
|
13044
|
-
const keyJsonPath =
|
|
13742
|
+
const keyJsonPath = join17(dataDir, dir, "dbs", "encryption-key.json");
|
|
13045
13743
|
try {
|
|
13046
|
-
const raw = await
|
|
13744
|
+
const raw = await readFile17(keyJsonPath, "utf-8");
|
|
13047
13745
|
const parsed = JSON.parse(raw);
|
|
13048
13746
|
if (!parsed.encryptionKey) continue;
|
|
13049
13747
|
const sodium = await getSodium();
|
|
@@ -13065,8 +13763,8 @@ async function runOverrideOnlyMigration(dataDir) {
|
|
|
13065
13763
|
const abDataDir = resolve6(dataDir);
|
|
13066
13764
|
const abCwd = resolve6(process.cwd());
|
|
13067
13765
|
const candidateDirs = [
|
|
13068
|
-
|
|
13069
|
-
|
|
13766
|
+
join17(abCwd, ".runeya", "dbs", "overrides"),
|
|
13767
|
+
join17(abDataDir, "dbs", "overrides")
|
|
13070
13768
|
];
|
|
13071
13769
|
let overridesDir = null;
|
|
13072
13770
|
let overrideFiles = [];
|
|
@@ -13082,7 +13780,7 @@ async function runOverrideOnlyMigration(dataDir) {
|
|
|
13082
13780
|
}
|
|
13083
13781
|
}
|
|
13084
13782
|
if (!overridesDir || overrideFiles.length === 0) return false;
|
|
13085
|
-
const legacyKeyPath =
|
|
13783
|
+
const legacyKeyPath = join17(overridesDir, "..", "encryption-key.json");
|
|
13086
13784
|
try {
|
|
13087
13785
|
await access2(legacyKeyPath);
|
|
13088
13786
|
return false;
|
|
@@ -13100,7 +13798,7 @@ async function runOverrideOnlyMigration(dataDir) {
|
|
|
13100
13798
|
const envNameToId = {};
|
|
13101
13799
|
let envsRaw;
|
|
13102
13800
|
try {
|
|
13103
|
-
envsRaw = JSON.parse(await
|
|
13801
|
+
envsRaw = JSON.parse(await readFile17(join17(abDataDir, "environments.json"), "utf-8"));
|
|
13104
13802
|
for (const _env of envsRaw) {
|
|
13105
13803
|
const env2 = _env;
|
|
13106
13804
|
if (env2["name"] && env2["id"]) {
|
|
@@ -13119,7 +13817,7 @@ async function runOverrideOnlyMigration(dataDir) {
|
|
|
13119
13817
|
let servicesRaw = [];
|
|
13120
13818
|
const serviceNameToId = {};
|
|
13121
13819
|
try {
|
|
13122
|
-
servicesRaw = JSON.parse(await
|
|
13820
|
+
servicesRaw = JSON.parse(await readFile17(join17(abDataDir, "services.json"), "utf-8"));
|
|
13123
13821
|
for (const _svc of servicesRaw) {
|
|
13124
13822
|
const svc = _svc;
|
|
13125
13823
|
if (svc["name"] && svc["id"]) serviceNameToId[svc["name"]] = svc["id"];
|
|
@@ -13136,7 +13834,7 @@ async function runOverrideOnlyMigration(dataDir) {
|
|
|
13136
13834
|
const envName = baseName.slice(0, -"-environment".length);
|
|
13137
13835
|
let data;
|
|
13138
13836
|
try {
|
|
13139
|
-
data = await decryptLegacyFile(
|
|
13837
|
+
data = await decryptLegacyFile(join17(overridesDir, file), sodiumKey, sodium);
|
|
13140
13838
|
} catch (err) {
|
|
13141
13839
|
console.warn(`[override-migration] Failed to decrypt ${file}: ${err.message}`);
|
|
13142
13840
|
continue;
|
|
@@ -13163,7 +13861,7 @@ async function runOverrideOnlyMigration(dataDir) {
|
|
|
13163
13861
|
}
|
|
13164
13862
|
}
|
|
13165
13863
|
if (Object.keys(flatOverrides).length > 0) {
|
|
13166
|
-
await atomicWrite(
|
|
13864
|
+
await atomicWrite(join17(abDataDir, "environment-overrides.json"), flatOverrides);
|
|
13167
13865
|
console.log(`[override-migration] Written ${Object.keys(flatOverrides).length} override(s) to environment-overrides.json`);
|
|
13168
13866
|
}
|
|
13169
13867
|
let envsModified = false;
|
|
@@ -13173,7 +13871,7 @@ async function runOverrideOnlyMigration(dataDir) {
|
|
|
13173
13871
|
const serviceName = baseName.slice(0, -"-envs".length);
|
|
13174
13872
|
let data;
|
|
13175
13873
|
try {
|
|
13176
|
-
data = await decryptLegacyFile(
|
|
13874
|
+
data = await decryptLegacyFile(join17(overridesDir, file), sodiumKey, sodium);
|
|
13177
13875
|
} catch (err) {
|
|
13178
13876
|
console.warn(`[override-migration] Failed to decrypt ${file}: ${err.message}`);
|
|
13179
13877
|
continue;
|
|
@@ -13232,7 +13930,7 @@ async function runOverrideOnlyMigration(dataDir) {
|
|
|
13232
13930
|
}
|
|
13233
13931
|
}
|
|
13234
13932
|
if (envsModified) {
|
|
13235
|
-
await atomicWrite(
|
|
13933
|
+
await atomicWrite(join17(abDataDir, "environments.json"), envsRaw);
|
|
13236
13934
|
console.log("[override-migration] Merged service overrides into environments.json");
|
|
13237
13935
|
}
|
|
13238
13936
|
const migratedDir = overridesDir.replace(/overrides$/, "overrides-migrated");
|
|
@@ -13247,27 +13945,32 @@ async function runOverrideOnlyMigration(dataDir) {
|
|
|
13247
13945
|
}
|
|
13248
13946
|
|
|
13249
13947
|
// ../server/src/services/server-state.ts
|
|
13250
|
-
import { join as
|
|
13948
|
+
import { join as join18 } from "path";
|
|
13251
13949
|
import { stat as stat7 } from "fs/promises";
|
|
13252
13950
|
var ENCRYPTED_KEY_FILENAME2 = ".encryption.key.enc";
|
|
13253
13951
|
var VERIFY_FILENAME2 = ".runeya-verify";
|
|
13254
13952
|
async function getServerState() {
|
|
13255
|
-
const dataDir =
|
|
13953
|
+
const dataDir = await keyRoot();
|
|
13256
13954
|
let hasEncFile = false;
|
|
13257
13955
|
let hasVerify = false;
|
|
13258
13956
|
let hasData = false;
|
|
13259
13957
|
try {
|
|
13260
|
-
await stat7(
|
|
13958
|
+
await stat7(join18(dataDir, ENCRYPTED_KEY_FILENAME2));
|
|
13261
13959
|
hasEncFile = true;
|
|
13262
13960
|
} catch {
|
|
13263
13961
|
}
|
|
13264
13962
|
try {
|
|
13265
|
-
await stat7(
|
|
13963
|
+
await stat7(join18(dataDir, VERIFY_FILENAME2));
|
|
13266
13964
|
hasVerify = true;
|
|
13267
13965
|
} catch {
|
|
13268
13966
|
}
|
|
13269
13967
|
try {
|
|
13270
|
-
await stat7(
|
|
13968
|
+
await stat7(join18(dataDir, "projects.json"));
|
|
13969
|
+
hasData = true;
|
|
13970
|
+
} catch {
|
|
13971
|
+
}
|
|
13972
|
+
try {
|
|
13973
|
+
await stat7(join18(dataDir, "project.json"));
|
|
13271
13974
|
hasData = true;
|
|
13272
13975
|
} catch {
|
|
13273
13976
|
}
|
|
@@ -13559,7 +14262,6 @@ function getOpenApiDocument() {
|
|
|
13559
14262
|
// ../server/src/create-server.ts
|
|
13560
14263
|
import rateLimit from "express-rate-limit";
|
|
13561
14264
|
import { existsSync as existsSync2, createReadStream } from "fs";
|
|
13562
|
-
import { mkdir as mkdir18 } from "fs/promises";
|
|
13563
14265
|
import multer from "multer";
|
|
13564
14266
|
|
|
13565
14267
|
// ../server/src/services/image-cleanup.service.ts
|
|
@@ -13650,7 +14352,8 @@ async function createLocalServer() {
|
|
|
13650
14352
|
generateJwtSecret();
|
|
13651
14353
|
const ciMode = await loadFromEnv();
|
|
13652
14354
|
await runLegacyMigration(env.DATA_DIR);
|
|
13653
|
-
|
|
14355
|
+
const keyDir = await keyRoot();
|
|
14356
|
+
if (!ciMode && !hasPendingLegacyKey() && !await hasEncryptedKeyFile(keyDir) && !await hasVerifyFile(keyDir)) {
|
|
13654
14357
|
const recoveredKey = await findLegacyKeyInBackup(env.DATA_DIR);
|
|
13655
14358
|
if (recoveredKey) {
|
|
13656
14359
|
setPendingLegacyKey(recoveredKey);
|
|
@@ -13665,10 +14368,12 @@ async function createLocalServer() {
|
|
|
13665
14368
|
console.log("[server] Premier lancement \u2014 setup requis");
|
|
13666
14369
|
}
|
|
13667
14370
|
}
|
|
13668
|
-
if (!existsSync2(env.DATA_DIR)) await mkdir18(env.DATA_DIR);
|
|
13669
14371
|
const absoluteDataDir = resolve5(env.DATA_DIR);
|
|
13670
|
-
console.log(`[server]
|
|
13671
|
-
|
|
14372
|
+
console.log(`[server] Local project directory: ${absoluteDataDir}`);
|
|
14373
|
+
console.log(`[server] Machine directory: ${machineRoot()}`);
|
|
14374
|
+
if (existsSync2(env.DATA_DIR)) {
|
|
14375
|
+
await ensureDataGitignore(env.DATA_DIR);
|
|
14376
|
+
}
|
|
13672
14377
|
await settingsManager.getSettings();
|
|
13673
14378
|
await migrationEngine.run();
|
|
13674
14379
|
await ensureFilePermissions();
|
|
@@ -13947,7 +14652,7 @@ async function createLocalServer() {
|
|
|
13947
14652
|
res.status(400).json({ error: "masterPassword invalide" });
|
|
13948
14653
|
return;
|
|
13949
14654
|
}
|
|
13950
|
-
const encKey = await readEncryptedKey(
|
|
14655
|
+
const encKey = await readEncryptedKey(await keyRoot(), masterPassword);
|
|
13951
14656
|
setEncryptionKeyInMemory(encKey);
|
|
13952
14657
|
await runPostUnlock();
|
|
13953
14658
|
const token = await signJwt();
|
|
@@ -13985,7 +14690,7 @@ async function createLocalServer() {
|
|
|
13985
14690
|
return;
|
|
13986
14691
|
}
|
|
13987
14692
|
encKey = Buffer.from(encryptionKeyHex, "hex");
|
|
13988
|
-
await verifyEncryptionKey(
|
|
14693
|
+
await verifyEncryptionKey(await keyRoot(), encKey);
|
|
13989
14694
|
} else {
|
|
13990
14695
|
if (body.encryptionKey !== void 0) {
|
|
13991
14696
|
const encryptionKeyHex = body.encryptionKey;
|
|
@@ -13999,14 +14704,14 @@ async function createLocalServer() {
|
|
|
13999
14704
|
encKey = randomBytes6(32);
|
|
14000
14705
|
}
|
|
14001
14706
|
}
|
|
14002
|
-
await writeEncryptedKey(
|
|
14003
|
-
const readBack = await readEncryptedKey(
|
|
14707
|
+
await writeEncryptedKey(await keyRoot(), masterPassword, encKey);
|
|
14708
|
+
const readBack = await readEncryptedKey(await keyRoot(), masterPassword);
|
|
14004
14709
|
if (!readBack.equals(encKey)) {
|
|
14005
14710
|
res.status(500).json({ error: "Erreur de v\xE9rification round-trip \u2014 op\xE9ration annul\xE9e" });
|
|
14006
14711
|
return;
|
|
14007
14712
|
}
|
|
14008
14713
|
if (!hasVerify) {
|
|
14009
|
-
await writeVerifyFile(
|
|
14714
|
+
await writeVerifyFile(await keyRoot(), encKey);
|
|
14010
14715
|
}
|
|
14011
14716
|
setEncryptionKeyInMemory(encKey);
|
|
14012
14717
|
if (isLegacyMigration) {
|
|
@@ -14429,4 +15134,4 @@ export {
|
|
|
14429
15134
|
createLocalServer,
|
|
14430
15135
|
pullEnv
|
|
14431
15136
|
};
|
|
14432
|
-
//# sourceMappingURL=src-
|
|
15137
|
+
//# sourceMappingURL=src-BCBD4MPB.js.map
|