termdock 1.4.178 → 1.4.179
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/dist/client/assets/App-DXq9kwY5.js +150 -0
- package/dist/client/assets/App-DXq9kwY5.js.br +0 -0
- package/dist/client/assets/App-DXq9kwY5.js.gz +0 -0
- package/dist/client/assets/{ChangeWalkthroughPanel-BNWqAnqc.js → ChangeWalkthroughPanel-DWUBYx6Q.js} +2 -2
- package/dist/client/assets/ChangeWalkthroughPanel-DWUBYx6Q.js.br +0 -0
- package/dist/client/assets/ChangeWalkthroughPanel-DWUBYx6Q.js.gz +0 -0
- package/dist/client/assets/{DagPlayground-IlTGwr-I.js → DagPlayground-DyczrcHM.js} +1 -1
- package/dist/client/assets/DagPlayground-DyczrcHM.js.br +0 -0
- package/dist/client/assets/DagPlayground-DyczrcHM.js.gz +0 -0
- package/dist/client/assets/{DiffLab-CwhNpJvo.js → DiffLab-D5wb-H7y.js} +1 -1
- package/dist/client/assets/DiffLab-D5wb-H7y.js.br +0 -0
- package/dist/client/assets/DiffLab-D5wb-H7y.js.gz +0 -0
- package/dist/client/assets/{DiffReview-DyhINv0F.js → DiffReview-CVIz-B5T.js} +1 -1
- package/dist/client/assets/DiffReview-CVIz-B5T.js.br +0 -0
- package/dist/client/assets/DiffReview-CVIz-B5T.js.gz +0 -0
- package/dist/client/assets/{DiffReviewLab-D9Kt5VwB.js → DiffReviewLab-99hTNRaj.js} +1 -1
- package/dist/client/assets/DiffReviewLab-99hTNRaj.js.br +0 -0
- package/dist/client/assets/DiffReviewLab-99hTNRaj.js.gz +0 -0
- package/dist/client/assets/{DiffViewer-3zz2PI-8.js → DiffViewer-BPor2_7o.js} +1 -1
- package/dist/client/assets/DiffViewer-BPor2_7o.js.br +0 -0
- package/dist/client/assets/DiffViewer-BPor2_7o.js.gz +0 -0
- package/dist/client/assets/{ModelPreview-CN8xsiGC.js → ModelPreview-DpImj4vY.js} +1 -1
- package/dist/client/assets/ModelPreview-DpImj4vY.js.br +0 -0
- package/dist/client/assets/ModelPreview-DpImj4vY.js.gz +0 -0
- package/dist/client/assets/RightSidebar-BW8SSsoA.js +79 -0
- package/dist/client/assets/RightSidebar-BW8SSsoA.js.br +0 -0
- package/dist/client/assets/RightSidebar-BW8SSsoA.js.gz +0 -0
- package/dist/client/assets/{abnfDiagram-VCTEODGH-DkdoJ2ZH.js → abnfDiagram-VCTEODGH-CNWmFVMh.js} +1 -1
- package/dist/client/assets/abnfDiagram-VCTEODGH-CNWmFVMh.js.br +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-CNWmFVMh.js.gz +0 -0
- package/dist/client/assets/app-Pl7tV8Be.js +34 -0
- package/dist/client/assets/app-Pl7tV8Be.js.br +0 -0
- package/dist/client/assets/app-Pl7tV8Be.js.gz +0 -0
- package/dist/client/assets/{arc-ctzz5PcK.js → arc-Z8mcHjh7.js} +1 -1
- package/dist/client/assets/arc-Z8mcHjh7.js.br +0 -0
- package/dist/client/assets/arc-Z8mcHjh7.js.gz +0 -0
- package/dist/client/assets/{architectureDiagram-5GKGNRK7-DNLcYfGQ.js → architectureDiagram-5GKGNRK7-DuxbAeFP.js} +1 -1
- package/dist/client/assets/architectureDiagram-5GKGNRK7-DuxbAeFP.js.br +0 -0
- package/dist/client/assets/architectureDiagram-5GKGNRK7-DuxbAeFP.js.gz +0 -0
- package/dist/client/assets/{blockDiagram-I7D4REHJ-Coye1ou5.js → blockDiagram-I7D4REHJ-SxAFw0An.js} +1 -1
- package/dist/client/assets/blockDiagram-I7D4REHJ-SxAFw0An.js.br +0 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-SxAFw0An.js.gz +0 -0
- package/dist/client/assets/{c4Diagram-7LVT6UL2-DTa8jyPp.js → c4Diagram-7LVT6UL2-CcRdiWo7.js} +1 -1
- package/dist/client/assets/{c4Diagram-7LVT6UL2-DTa8jyPp.js.br → c4Diagram-7LVT6UL2-CcRdiWo7.js.br} +0 -0
- package/dist/client/assets/c4Diagram-7LVT6UL2-CcRdiWo7.js.gz +0 -0
- package/dist/client/assets/channel-nXn0ntBk.js +1 -0
- package/dist/client/assets/{chunk-2Q5K7J3B-BpDyjNfs.js → chunk-2Q5K7J3B-DJ_qzvZZ.js} +1 -1
- package/dist/client/assets/{chunk-5VM5RSS4-DgwdqXKl.js → chunk-5VM5RSS4-l3H0C2ci.js} +1 -1
- package/dist/client/assets/{chunk-F27PBJKO-DWf2gZ96.js → chunk-F27PBJKO-B7xASv8n.js} +1 -1
- package/dist/client/assets/chunk-F27PBJKO-B7xASv8n.js.br +0 -0
- package/dist/client/assets/chunk-F27PBJKO-B7xASv8n.js.gz +0 -0
- package/dist/client/assets/{chunk-IMKFNOWR-l42o_E2c.js → chunk-IMKFNOWR-Wg5gR7sZ.js} +1 -1
- package/dist/client/assets/chunk-IMKFNOWR-Wg5gR7sZ.js.br +0 -0
- package/dist/client/assets/chunk-IMKFNOWR-Wg5gR7sZ.js.gz +0 -0
- package/dist/client/assets/{chunk-JWPE2WC7-8CkEf05l.js → chunk-JWPE2WC7-BlXUS79m.js} +1 -1
- package/dist/client/assets/{chunk-POPQ4Y6H-BKKOfbdg.js → chunk-POPQ4Y6H-CU38G8hb.js} +1 -1
- package/dist/client/assets/{chunk-SVP7TREG-RORrf2aM.js → chunk-SVP7TREG-D-yZlXqX.js} +1 -1
- package/dist/client/assets/chunk-SVP7TREG-D-yZlXqX.js.br +0 -0
- package/dist/client/assets/chunk-SVP7TREG-D-yZlXqX.js.gz +0 -0
- package/dist/client/assets/{chunk-TICWLB2K-hPqafEtC.js → chunk-TICWLB2K-DrOYmcBB.js} +1 -1
- package/dist/client/assets/{chunk-TICWLB2K-hPqafEtC.js.br → chunk-TICWLB2K-DrOYmcBB.js.br} +0 -0
- package/dist/client/assets/chunk-TICWLB2K-DrOYmcBB.js.gz +0 -0
- package/dist/client/assets/{chunk-XXDRQBXY-CrvjJwms.js → chunk-XXDRQBXY-BbsHtVFa.js} +1 -1
- package/dist/client/assets/classDiagram-ZZMXUADV-DxqN38m7.js +1 -0
- package/dist/client/assets/classDiagram-v2-VYDZK3BY-DxqN38m7.js +1 -0
- package/dist/client/assets/{cose-bilkent-JH36ORCC-CwRSiQLa.js → cose-bilkent-JH36ORCC-_wKUNL8T.js} +1 -1
- package/dist/client/assets/cose-bilkent-JH36ORCC-_wKUNL8T.js.br +0 -0
- package/dist/client/assets/cose-bilkent-JH36ORCC-_wKUNL8T.js.gz +0 -0
- package/dist/client/assets/{cynefin-OW5HDTMX-CXnppZ8I.js → cynefin-OW5HDTMX-CPr--Wnc.js} +1 -1
- package/dist/client/assets/cynefin-OW5HDTMX-CPr--Wnc.js.br +0 -0
- package/dist/client/assets/cynefin-OW5HDTMX-CPr--Wnc.js.gz +0 -0
- package/dist/client/assets/{cynefinDiagram-5FMLGOSQ-BSDrHSIA.js → cynefinDiagram-5FMLGOSQ-5ygpPOWT.js} +1 -1
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-5ygpPOWT.js.br +0 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-5ygpPOWT.js.gz +0 -0
- package/dist/client/assets/{dagre-GXQ25YYZ-d-5j4vkq.js → dagre-GXQ25YYZ-Cvx65nHO.js} +1 -1
- package/dist/client/assets/dagre-GXQ25YYZ-Cvx65nHO.js.br +0 -0
- package/dist/client/assets/dagre-GXQ25YYZ-Cvx65nHO.js.gz +0 -0
- package/dist/client/assets/{diagram-S7CK7UJ4-DizMBpGW.js → diagram-S7CK7UJ4-Dw5cp_BA.js} +1 -1
- package/dist/client/assets/diagram-S7CK7UJ4-Dw5cp_BA.js.br +0 -0
- package/dist/client/assets/diagram-S7CK7UJ4-Dw5cp_BA.js.gz +0 -0
- package/dist/client/assets/{diagram-UQ7AKVKN-CNQK26rH.js → diagram-UQ7AKVKN-D8BZ5Tp3.js} +1 -1
- package/dist/client/assets/diagram-UQ7AKVKN-D8BZ5Tp3.js.br +0 -0
- package/dist/client/assets/diagram-UQ7AKVKN-D8BZ5Tp3.js.gz +0 -0
- package/dist/client/assets/{diagram-VSXAHHWV-Dua-2_46.js → diagram-VSXAHHWV-BxJD7ZWH.js} +1 -1
- package/dist/client/assets/diagram-VSXAHHWV-BxJD7ZWH.js.br +0 -0
- package/dist/client/assets/diagram-VSXAHHWV-BxJD7ZWH.js.gz +0 -0
- package/dist/client/assets/{diagram-VX7I27RA-CIq-4729.js → diagram-VX7I27RA-CvzlU0bI.js} +1 -1
- package/dist/client/assets/diagram-VX7I27RA-CvzlU0bI.js.br +0 -0
- package/dist/client/assets/diagram-VX7I27RA-CvzlU0bI.js.gz +0 -0
- package/dist/client/assets/{diagram-Z3DM3KII-BukQ0u-x.js → diagram-Z3DM3KII-Be4HdFlL.js} +1 -1
- package/dist/client/assets/diagram-Z3DM3KII-Be4HdFlL.js.br +0 -0
- package/dist/client/assets/diagram-Z3DM3KII-Be4HdFlL.js.gz +0 -0
- package/dist/client/assets/{ebnfDiagram-PWID7BFC-D2zRkRBI.js → ebnfDiagram-PWID7BFC-C_wrrPjF.js} +1 -1
- package/dist/client/assets/ebnfDiagram-PWID7BFC-C_wrrPjF.js.br +0 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-C_wrrPjF.js.gz +0 -0
- package/dist/client/assets/{erDiagram-RLTQ6QDP-DbX6A3ls.js → erDiagram-RLTQ6QDP-IHyLgV6M.js} +1 -1
- package/dist/client/assets/erDiagram-RLTQ6QDP-IHyLgV6M.js.br +0 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-IHyLgV6M.js.gz +0 -0
- package/dist/client/assets/{flowDiagram-HODETNUW-YECDWA2G.js → flowDiagram-HODETNUW-CpfW88Uc.js} +1 -1
- package/dist/client/assets/flowDiagram-HODETNUW-CpfW88Uc.js.br +0 -0
- package/dist/client/assets/flowDiagram-HODETNUW-CpfW88Uc.js.gz +0 -0
- package/dist/client/assets/{ganttDiagram-EL5Y4UJY-D3EJdUov.js → ganttDiagram-EL5Y4UJY-CsbQ6KLb.js} +1 -1
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-CsbQ6KLb.js.br +0 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-CsbQ6KLb.js.gz +0 -0
- package/dist/client/assets/{gestureArbiter-D3IZpZS7.js → gestureArbiter-CJQ8K48V.js} +1 -1
- package/dist/client/assets/gestureArbiter-CJQ8K48V.js.br +0 -0
- package/dist/client/assets/gestureArbiter-CJQ8K48V.js.gz +0 -0
- package/dist/client/assets/{gitGraphDiagram-WWUBYQGX-tr_HKoQK.js → gitGraphDiagram-WWUBYQGX-DJZqSGtH.js} +1 -1
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-DJZqSGtH.js.br +0 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-DJZqSGtH.js.gz +0 -0
- package/dist/client/assets/{index-C4Ny0NDQ.css → index-PWu1VEN_.css} +1 -1
- package/dist/client/assets/index-PWu1VEN_.css.br +0 -0
- package/dist/client/assets/index-PWu1VEN_.css.gz +0 -0
- package/dist/client/assets/{infoDiagram-27XIBGKW-xBV63YYg.js → infoDiagram-27XIBGKW-D6sSgR2Y.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-5VMMS53U-BrYSZu1R.js → ishikawaDiagram-5VMMS53U-BJJSUXep.js} +1 -1
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-BJJSUXep.js.br +0 -0
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-BJJSUXep.js.gz +0 -0
- package/dist/client/assets/{journeyDiagram-3NMN7TZE-C9Ft--aA.js → journeyDiagram-3NMN7TZE-ou1XS2DP.js} +1 -1
- package/dist/client/assets/journeyDiagram-3NMN7TZE-ou1XS2DP.js.br +0 -0
- package/dist/client/assets/journeyDiagram-3NMN7TZE-ou1XS2DP.js.gz +0 -0
- package/dist/client/assets/{kanban-definition-UXKFOSKX-FCxqSu11.js → kanban-definition-UXKFOSKX-BFQGYQzd.js} +1 -1
- package/dist/client/assets/kanban-definition-UXKFOSKX-BFQGYQzd.js.br +0 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-BFQGYQzd.js.gz +0 -0
- package/dist/client/assets/{layout-BqjIyPqO.js → layout-zUU3xcam.js} +1 -1
- package/dist/client/assets/layout-zUU3xcam.js.br +0 -0
- package/dist/client/assets/layout-zUU3xcam.js.gz +0 -0
- package/dist/client/assets/{linear-CEAGQX5N.js → linear-CYwYQI2I.js} +1 -1
- package/dist/client/assets/linear-CYwYQI2I.js.br +0 -0
- package/dist/client/assets/linear-CYwYQI2I.js.gz +0 -0
- package/dist/client/assets/{mermaid.core-DvAY78t_.js → mermaid.core-91Llsw5j.js} +5 -5
- package/dist/client/assets/mermaid.core-91Llsw5j.js.br +0 -0
- package/dist/client/assets/mermaid.core-91Llsw5j.js.gz +0 -0
- package/dist/client/assets/{mindmap-definition-YA3MSWOX-D7_qIQ9H.js → mindmap-definition-YA3MSWOX-RWG074lb.js} +1 -1
- package/dist/client/assets/mindmap-definition-YA3MSWOX-RWG074lb.js.br +0 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-RWG074lb.js.gz +0 -0
- package/dist/client/assets/{passwordBootstrap-Dksfdvk3.js → passwordBootstrap-DRknuNUN.js} +1 -1
- package/dist/client/assets/passwordBootstrap-DRknuNUN.js.br +0 -0
- package/dist/client/assets/passwordBootstrap-DRknuNUN.js.gz +0 -0
- package/dist/client/assets/{pegDiagram-XKGWAZYB-DeTIBKGC.js → pegDiagram-XKGWAZYB-Dyh2rzGS.js} +1 -1
- package/dist/client/assets/pegDiagram-XKGWAZYB-Dyh2rzGS.js.br +0 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-Dyh2rzGS.js.gz +0 -0
- package/dist/client/assets/{pieDiagram-E7YTZNPT-z5IcoVwx.js → pieDiagram-E7YTZNPT-CL3UcUfh.js} +1 -1
- package/dist/client/assets/pieDiagram-E7YTZNPT-CL3UcUfh.js.br +0 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-CL3UcUfh.js.gz +0 -0
- package/dist/client/assets/{quadrantDiagram-AXDQQJYC-B-zCZvES.js → quadrantDiagram-AXDQQJYC-DOkejMel.js} +1 -1
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-DOkejMel.js.br +0 -0
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-DOkejMel.js.gz +0 -0
- package/dist/client/assets/{railroadDiagram-O6MQD6OU-DbpeB7P2.js → railroadDiagram-O6MQD6OU-BC780Uh3.js} +1 -1
- package/dist/client/assets/railroadDiagram-O6MQD6OU-BC780Uh3.js.br +0 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-BC780Uh3.js.gz +0 -0
- package/dist/client/assets/{referenceLongPress-p56g9xFb.js → referenceLongPress-Dlc0425_.js} +1 -1
- package/dist/client/assets/referenceLongPress-Dlc0425_.js.br +0 -0
- package/dist/client/assets/referenceLongPress-Dlc0425_.js.gz +0 -0
- package/dist/client/assets/{requirementDiagram-BXWQKSXE-UhJYMJ_U.js → requirementDiagram-BXWQKSXE-CwCGr0qR.js} +1 -1
- package/dist/client/assets/requirementDiagram-BXWQKSXE-CwCGr0qR.js.br +0 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-CwCGr0qR.js.gz +0 -0
- package/dist/client/assets/{sankeyDiagram-P5KCCOFB-xKsuFR17.js → sankeyDiagram-P5KCCOFB-CYDYBwJQ.js} +1 -1
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-CYDYBwJQ.js.br +0 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-CYDYBwJQ.js.gz +0 -0
- package/dist/client/assets/{sequenceDiagram-WJ2MYXX4-DrjBl65C.js → sequenceDiagram-WJ2MYXX4-DhHsZi3K.js} +1 -1
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-DhHsZi3K.js.br +0 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-DhHsZi3K.js.gz +0 -0
- package/dist/client/assets/{sizeCapture-INFHLROL-BIlGJ9tm.js → sizeCapture-INFHLROL-4oUXWvYA.js} +1 -1
- package/dist/client/assets/sizeCapture-INFHLROL-4oUXWvYA.js.br +0 -0
- package/dist/client/assets/sizeCapture-INFHLROL-4oUXWvYA.js.gz +0 -0
- package/dist/client/assets/{sparkles-B-uEi0Pa.js → sparkles-gqiuBkfP.js} +1 -1
- package/dist/client/assets/{stateDiagram-D77RDMKH-C3k9XvA6.js → stateDiagram-D77RDMKH-7hUELZGo.js} +1 -1
- package/dist/client/assets/stateDiagram-D77RDMKH-7hUELZGo.js.br +0 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-7hUELZGo.js.gz +0 -0
- package/dist/client/assets/stateDiagram-v2-MP3YSRHH-MO84Jqt6.js +1 -0
- package/dist/client/assets/{swimlanes-42K2YHIH-BN-x1rXp.js → swimlanes-42K2YHIH-E1qoordP.js} +1 -1
- package/dist/client/assets/swimlanes-42K2YHIH-E1qoordP.js.br +0 -0
- package/dist/client/assets/swimlanes-42K2YHIH-E1qoordP.js.gz +0 -0
- package/dist/client/assets/swimlanesDiagram-VR7AAH4N-DnSXMnZQ.js +8 -0
- package/dist/client/assets/{timeline-definition-24CTP7MA-Dcedjtic.js → timeline-definition-24CTP7MA-CTYyCnUW.js} +1 -1
- package/dist/client/assets/timeline-definition-24CTP7MA-CTYyCnUW.js.br +0 -0
- package/dist/client/assets/timeline-definition-24CTP7MA-CTYyCnUW.js.gz +0 -0
- package/dist/client/assets/{triangle-alert-BSNAwyll.js → triangle-alert-DrBuMUPw.js} +1 -1
- package/dist/client/assets/{vennDiagram-4TSXK5OY-DAIk-5Rt.js → vennDiagram-4TSXK5OY-DVWbhWsS.js} +1 -1
- package/dist/client/assets/vennDiagram-4TSXK5OY-DVWbhWsS.js.br +0 -0
- package/dist/client/assets/vennDiagram-4TSXK5OY-DVWbhWsS.js.gz +0 -0
- package/dist/client/assets/{wardleyDiagram-VM6X3IG4-3iXOTpAg.js → wardleyDiagram-VM6X3IG4-BFAwOxhc.js} +1 -1
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-BFAwOxhc.js.br +0 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-BFAwOxhc.js.gz +0 -0
- package/dist/client/assets/{xychartDiagram-S5SC5T6Z-OD1a_9Qg.js → xychartDiagram-S5SC5T6Z-DYXOiBiX.js} +1 -1
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-DYXOiBiX.js.br +0 -0
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-DYXOiBiX.js.gz +0 -0
- package/dist/client/index.html +2 -2
- package/dist/client/index.html.br +0 -0
- package/dist/client/index.html.gz +0 -0
- package/dist/client/sw-notifications.js +5 -45
- package/dist/client/sw-notifications.js.br +0 -0
- package/dist/client/sw-notifications.js.gz +0 -0
- package/dist/client/sw.js +1 -1
- package/dist/client/sw.js.br +0 -0
- package/dist/client/sw.js.gz +0 -0
- package/dist/server/agent/collaborationCli.js +10 -3
- package/dist/server/agent/collaborationDeliveryWorker.js +154 -0
- package/dist/server/agent/collaborationRoutes.js +15 -4
- package/dist/server/agent/collaborationRouting.js +94 -0
- package/dist/server/agent/collaborationStore.js +6 -1
- package/dist/server/agent/collaborationTmuxDelivery.js +10 -0
- package/dist/server/agent/sessionBindingRecovery.js +59 -0
- package/dist/server/entry.js +30 -13
- package/dist/server/federation/desktopTargets.js +64 -0
- package/dist/server/federation/directRoutes.js +27 -19
- package/dist/server/federation/routeAccess.js +14 -7
- package/dist/server/federation/trustedCa.js +55 -0
- package/dist/server/routes/terminal.js +242 -67
- package/package.json +1 -1
- package/runtime-manifest.json +3 -3
- package/dist/client/assets/App-CAEtVL8Z.js +0 -150
- package/dist/client/assets/App-CAEtVL8Z.js.br +0 -0
- package/dist/client/assets/App-CAEtVL8Z.js.gz +0 -0
- package/dist/client/assets/ChangeWalkthroughPanel-BNWqAnqc.js.br +0 -0
- package/dist/client/assets/ChangeWalkthroughPanel-BNWqAnqc.js.gz +0 -0
- package/dist/client/assets/DagPlayground-IlTGwr-I.js.br +0 -0
- package/dist/client/assets/DagPlayground-IlTGwr-I.js.gz +0 -0
- package/dist/client/assets/DiffLab-CwhNpJvo.js.br +0 -0
- package/dist/client/assets/DiffLab-CwhNpJvo.js.gz +0 -0
- package/dist/client/assets/DiffReview-DyhINv0F.js.br +0 -0
- package/dist/client/assets/DiffReview-DyhINv0F.js.gz +0 -0
- package/dist/client/assets/DiffReviewLab-D9Kt5VwB.js.br +0 -0
- package/dist/client/assets/DiffReviewLab-D9Kt5VwB.js.gz +0 -0
- package/dist/client/assets/DiffViewer-3zz2PI-8.js.br +0 -0
- package/dist/client/assets/DiffViewer-3zz2PI-8.js.gz +0 -0
- package/dist/client/assets/ModelPreview-CN8xsiGC.js.br +0 -0
- package/dist/client/assets/ModelPreview-CN8xsiGC.js.gz +0 -0
- package/dist/client/assets/RightSidebar-DQk1cwKH.js +0 -79
- package/dist/client/assets/RightSidebar-DQk1cwKH.js.br +0 -0
- package/dist/client/assets/RightSidebar-DQk1cwKH.js.gz +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-DkdoJ2ZH.js.br +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-DkdoJ2ZH.js.gz +0 -0
- package/dist/client/assets/app-B42CoF1r.js +0 -34
- package/dist/client/assets/app-B42CoF1r.js.br +0 -0
- package/dist/client/assets/app-B42CoF1r.js.gz +0 -0
- package/dist/client/assets/arc-ctzz5PcK.js.br +0 -0
- package/dist/client/assets/arc-ctzz5PcK.js.gz +0 -0
- package/dist/client/assets/architectureDiagram-5GKGNRK7-DNLcYfGQ.js.br +0 -0
- package/dist/client/assets/architectureDiagram-5GKGNRK7-DNLcYfGQ.js.gz +0 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-Coye1ou5.js.br +0 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-Coye1ou5.js.gz +0 -0
- package/dist/client/assets/c4Diagram-7LVT6UL2-DTa8jyPp.js.gz +0 -0
- package/dist/client/assets/channel-CzvSDv06.js +0 -1
- package/dist/client/assets/chunk-F27PBJKO-DWf2gZ96.js.br +0 -0
- package/dist/client/assets/chunk-F27PBJKO-DWf2gZ96.js.gz +0 -0
- package/dist/client/assets/chunk-IMKFNOWR-l42o_E2c.js.br +0 -0
- package/dist/client/assets/chunk-IMKFNOWR-l42o_E2c.js.gz +0 -0
- package/dist/client/assets/chunk-SVP7TREG-RORrf2aM.js.br +0 -0
- package/dist/client/assets/chunk-SVP7TREG-RORrf2aM.js.gz +0 -0
- package/dist/client/assets/chunk-TICWLB2K-hPqafEtC.js.gz +0 -0
- package/dist/client/assets/classDiagram-ZZMXUADV-CDknJfE0.js +0 -1
- package/dist/client/assets/classDiagram-v2-VYDZK3BY-CDknJfE0.js +0 -1
- package/dist/client/assets/cose-bilkent-JH36ORCC-CwRSiQLa.js.br +0 -0
- package/dist/client/assets/cose-bilkent-JH36ORCC-CwRSiQLa.js.gz +0 -0
- package/dist/client/assets/cynefin-OW5HDTMX-CXnppZ8I.js.br +0 -0
- package/dist/client/assets/cynefin-OW5HDTMX-CXnppZ8I.js.gz +0 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-BSDrHSIA.js.br +0 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-BSDrHSIA.js.gz +0 -0
- package/dist/client/assets/dagre-GXQ25YYZ-d-5j4vkq.js.br +0 -0
- package/dist/client/assets/dagre-GXQ25YYZ-d-5j4vkq.js.gz +0 -0
- package/dist/client/assets/diagram-S7CK7UJ4-DizMBpGW.js.br +0 -0
- package/dist/client/assets/diagram-S7CK7UJ4-DizMBpGW.js.gz +0 -0
- package/dist/client/assets/diagram-UQ7AKVKN-CNQK26rH.js.br +0 -0
- package/dist/client/assets/diagram-UQ7AKVKN-CNQK26rH.js.gz +0 -0
- package/dist/client/assets/diagram-VSXAHHWV-Dua-2_46.js.br +0 -0
- package/dist/client/assets/diagram-VSXAHHWV-Dua-2_46.js.gz +0 -0
- package/dist/client/assets/diagram-VX7I27RA-CIq-4729.js.br +0 -0
- package/dist/client/assets/diagram-VX7I27RA-CIq-4729.js.gz +0 -0
- package/dist/client/assets/diagram-Z3DM3KII-BukQ0u-x.js.br +0 -0
- package/dist/client/assets/diagram-Z3DM3KII-BukQ0u-x.js.gz +0 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-D2zRkRBI.js.br +0 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-D2zRkRBI.js.gz +0 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-DbX6A3ls.js.br +0 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-DbX6A3ls.js.gz +0 -0
- package/dist/client/assets/flowDiagram-HODETNUW-YECDWA2G.js.br +0 -0
- package/dist/client/assets/flowDiagram-HODETNUW-YECDWA2G.js.gz +0 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-D3EJdUov.js.br +0 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-D3EJdUov.js.gz +0 -0
- package/dist/client/assets/gestureArbiter-D3IZpZS7.js.br +0 -0
- package/dist/client/assets/gestureArbiter-D3IZpZS7.js.gz +0 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-tr_HKoQK.js.br +0 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-tr_HKoQK.js.gz +0 -0
- package/dist/client/assets/index-C4Ny0NDQ.css.br +0 -0
- package/dist/client/assets/index-C4Ny0NDQ.css.gz +0 -0
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-BrYSZu1R.js.br +0 -0
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-BrYSZu1R.js.gz +0 -0
- package/dist/client/assets/journeyDiagram-3NMN7TZE-C9Ft--aA.js.br +0 -0
- package/dist/client/assets/journeyDiagram-3NMN7TZE-C9Ft--aA.js.gz +0 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-FCxqSu11.js.br +0 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-FCxqSu11.js.gz +0 -0
- package/dist/client/assets/layout-BqjIyPqO.js.br +0 -0
- package/dist/client/assets/layout-BqjIyPqO.js.gz +0 -0
- package/dist/client/assets/linear-CEAGQX5N.js.br +0 -0
- package/dist/client/assets/linear-CEAGQX5N.js.gz +0 -0
- package/dist/client/assets/mermaid.core-DvAY78t_.js.br +0 -0
- package/dist/client/assets/mermaid.core-DvAY78t_.js.gz +0 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-D7_qIQ9H.js.br +0 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-D7_qIQ9H.js.gz +0 -0
- package/dist/client/assets/passwordBootstrap-Dksfdvk3.js.br +0 -0
- package/dist/client/assets/passwordBootstrap-Dksfdvk3.js.gz +0 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-DeTIBKGC.js.br +0 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-DeTIBKGC.js.gz +0 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-z5IcoVwx.js.br +0 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-z5IcoVwx.js.gz +0 -0
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-B-zCZvES.js.br +0 -0
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-B-zCZvES.js.gz +0 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-DbpeB7P2.js.br +0 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-DbpeB7P2.js.gz +0 -0
- package/dist/client/assets/referenceLongPress-p56g9xFb.js.br +0 -0
- package/dist/client/assets/referenceLongPress-p56g9xFb.js.gz +0 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-UhJYMJ_U.js.br +0 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-UhJYMJ_U.js.gz +0 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-xKsuFR17.js.br +0 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-xKsuFR17.js.gz +0 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-DrjBl65C.js.br +0 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-DrjBl65C.js.gz +0 -0
- package/dist/client/assets/sizeCapture-INFHLROL-BIlGJ9tm.js.br +0 -0
- package/dist/client/assets/sizeCapture-INFHLROL-BIlGJ9tm.js.gz +0 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-C3k9XvA6.js.br +0 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-C3k9XvA6.js.gz +0 -0
- package/dist/client/assets/stateDiagram-v2-MP3YSRHH-B7GnCu2o.js +0 -1
- package/dist/client/assets/swimlanes-42K2YHIH-BN-x1rXp.js.br +0 -0
- package/dist/client/assets/swimlanes-42K2YHIH-BN-x1rXp.js.gz +0 -0
- package/dist/client/assets/swimlanesDiagram-VR7AAH4N-BH_gm34Y.js +0 -8
- package/dist/client/assets/timeline-definition-24CTP7MA-Dcedjtic.js.br +0 -0
- package/dist/client/assets/timeline-definition-24CTP7MA-Dcedjtic.js.gz +0 -0
- package/dist/client/assets/vennDiagram-4TSXK5OY-DAIk-5Rt.js.br +0 -0
- package/dist/client/assets/vennDiagram-4TSXK5OY-DAIk-5Rt.js.gz +0 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-3iXOTpAg.js.br +0 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-3iXOTpAg.js.gz +0 -0
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-OD1a_9Qg.js.br +0 -0
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-OD1a_9Qg.js.gz +0 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
export class CollaborationDeliveryWorker {
|
|
2
|
+
options;
|
|
3
|
+
running = new Map();
|
|
4
|
+
states = new Map();
|
|
5
|
+
failures = new Map();
|
|
6
|
+
submitted = new Set();
|
|
7
|
+
timer = null;
|
|
8
|
+
ticking = null;
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.options = options;
|
|
11
|
+
}
|
|
12
|
+
start() {
|
|
13
|
+
if (this.timer)
|
|
14
|
+
return;
|
|
15
|
+
this.timer = setInterval(() => { void this.tick(); }, 2_000);
|
|
16
|
+
this.timer.unref?.();
|
|
17
|
+
void this.tick();
|
|
18
|
+
}
|
|
19
|
+
stop() {
|
|
20
|
+
if (this.timer)
|
|
21
|
+
clearInterval(this.timer);
|
|
22
|
+
this.timer = null;
|
|
23
|
+
}
|
|
24
|
+
state(id) {
|
|
25
|
+
const state = this.states.get(id);
|
|
26
|
+
return state && Date.now() - state.checkedAt < 15_000 ? state
|
|
27
|
+
: { state: 'recovering', reason: 'ROUTE_NOT_CHECKED', checkedAt: state?.checkedAt ?? null };
|
|
28
|
+
}
|
|
29
|
+
wake(id) {
|
|
30
|
+
void this.run(id).catch(this.options.onError);
|
|
31
|
+
}
|
|
32
|
+
tick() {
|
|
33
|
+
if (this.ticking)
|
|
34
|
+
return this.ticking;
|
|
35
|
+
this.ticking = this.tickOnce().finally(() => { this.ticking = null; });
|
|
36
|
+
return this.ticking;
|
|
37
|
+
}
|
|
38
|
+
async tickOnce() {
|
|
39
|
+
try {
|
|
40
|
+
const ids = [...new Set([...this.options.peers(), ...this.options.store.pendingRecipients()])].filter(this.options.isLocal);
|
|
41
|
+
// Bound process probes/attachments while keeping a broken peer from
|
|
42
|
+
// serially blocking the entire queue.
|
|
43
|
+
let cursor = 0;
|
|
44
|
+
await Promise.all(Array.from({ length: Math.min(4, ids.length) }, async () => {
|
|
45
|
+
while (cursor < ids.length)
|
|
46
|
+
await this.run(ids[cursor++]).catch(this.options.onError);
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
this.options.onError(error);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
run(id) {
|
|
54
|
+
const running = this.running.get(id);
|
|
55
|
+
if (running)
|
|
56
|
+
return running;
|
|
57
|
+
if (!this.options.isLocal(id))
|
|
58
|
+
return Promise.resolve();
|
|
59
|
+
const operation = Promise.resolve().then(() => this.deliver(id)).finally(() => {
|
|
60
|
+
if (this.running.get(id) === operation)
|
|
61
|
+
this.running.delete(id);
|
|
62
|
+
});
|
|
63
|
+
this.running.set(id, operation);
|
|
64
|
+
return operation;
|
|
65
|
+
}
|
|
66
|
+
reconfigure(id, configure) {
|
|
67
|
+
const previous = this.running.get(id) ?? Promise.resolve();
|
|
68
|
+
const operation = previous.catch(() => undefined).then(async () => {
|
|
69
|
+
await configure();
|
|
70
|
+
this.failures.delete(id);
|
|
71
|
+
this.states.delete(id);
|
|
72
|
+
for (const message of this.options.store.inbox(id, { pendingOnly: true, limit: 200 })) {
|
|
73
|
+
const diagnostic = this.options.store.diagnostic(message.id);
|
|
74
|
+
if (diagnostic)
|
|
75
|
+
this.options.store.recordTransport(message.id, { ...diagnostic, next_retry_at: null, last_error: null });
|
|
76
|
+
}
|
|
77
|
+
}).finally(() => {
|
|
78
|
+
if (this.running.get(id) === operation)
|
|
79
|
+
this.running.delete(id);
|
|
80
|
+
});
|
|
81
|
+
this.running.set(id, operation);
|
|
82
|
+
return operation;
|
|
83
|
+
}
|
|
84
|
+
async deliver(id) {
|
|
85
|
+
const { store } = this.options;
|
|
86
|
+
const first = store.inbox(id, { pendingOnly: true, limit: 1 })[0];
|
|
87
|
+
if (!first && Date.now() - (this.states.get(id)?.checkedAt ?? 0) < 5_000)
|
|
88
|
+
return;
|
|
89
|
+
if (first && this.submitted.has(first.id)) {
|
|
90
|
+
this.complete(id, first);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (first && (store.diagnostic(first.id)?.next_retry_at ?? 0) > Date.now())
|
|
94
|
+
return;
|
|
95
|
+
this.states.set(id, { state: 'recovering', reason: null, checkedAt: Date.now() });
|
|
96
|
+
let route;
|
|
97
|
+
try {
|
|
98
|
+
route = await this.options.resolve(id);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
route = { state: 'unavailable', reason: `ROUTE_RECOVERY_FAILED: ${error instanceof Error ? error.message : String(error)}` };
|
|
102
|
+
}
|
|
103
|
+
this.states.set(id, { state: route.state, reason: route.reason ?? null, checkedAt: Date.now() });
|
|
104
|
+
// Re-read after asynchronous recovery: a consumer may have read the
|
|
105
|
+
// message, or its TTL may have elapsed while tmux was being inspected.
|
|
106
|
+
const pending = store.inbox(id, { pendingOnly: true, limit: 1 });
|
|
107
|
+
if (!pending.length)
|
|
108
|
+
return;
|
|
109
|
+
if (route.state !== 'ready' || !route.write) {
|
|
110
|
+
this.retry(id, pending, route.reason ?? route.state, false);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const message = pending[0];
|
|
114
|
+
if (!this.submitted.has(message.id)) {
|
|
115
|
+
store.recordTransport(message.id, {
|
|
116
|
+
relay_online: null, peer_reachable: true,
|
|
117
|
+
attempt_count: (store.diagnostic(message.id)?.attempt_count ?? 0) + 1,
|
|
118
|
+
next_retry_at: Date.now() + 2_000, last_error: 'DELIVERY_IN_PROGRESS', checked_at: Date.now(),
|
|
119
|
+
});
|
|
120
|
+
try {
|
|
121
|
+
await route.write(pending);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
this.retry(id, pending, `TERMINAL_WRITE_FAILED: ${error instanceof Error ? error.message : String(error)}`, true);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
this.submitted.add(message.id);
|
|
128
|
+
}
|
|
129
|
+
// If persistence fails after writing, the in-process guard avoids a
|
|
130
|
+
// second write on retry. Across a crash, transport is at-least-once.
|
|
131
|
+
this.complete(id, message);
|
|
132
|
+
}
|
|
133
|
+
complete(id, message) {
|
|
134
|
+
const { store } = this.options;
|
|
135
|
+
store.markDelivered([message.id]);
|
|
136
|
+
this.submitted.delete(message.id);
|
|
137
|
+
store.recordTransport(message.id, {
|
|
138
|
+
relay_online: null, peer_reachable: true, attempt_count: store.diagnostic(message.id)?.attempt_count ?? 1,
|
|
139
|
+
next_retry_at: null, last_error: null, checked_at: Date.now(),
|
|
140
|
+
});
|
|
141
|
+
this.failures.delete(id);
|
|
142
|
+
}
|
|
143
|
+
retry(id, messages, reason, reachable) {
|
|
144
|
+
const failures = (this.failures.get(id) ?? 0) + 1;
|
|
145
|
+
this.failures.set(id, failures);
|
|
146
|
+
const next = Date.now() + Math.min(30_000, 2_000 * 2 ** Math.min(failures - 1, 4));
|
|
147
|
+
for (const message of messages)
|
|
148
|
+
this.options.store.recordTransport(message.id, {
|
|
149
|
+
relay_online: null, peer_reachable: reachable,
|
|
150
|
+
attempt_count: this.options.store.diagnostic(message.id)?.attempt_count ?? 0,
|
|
151
|
+
next_retry_at: next, last_error: reason, checked_at: Date.now(),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -1,19 +1,28 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import { COLLAB_LIMITS, CollaborationError, extrasFromBody } from './collaborationProtocol.js';
|
|
3
|
-
export function collaborationRoutes({ store, resolveSession, deliver }) {
|
|
3
|
+
export function collaborationRoutes({ store, resolveSession, deliver, rebind }) {
|
|
4
4
|
const router = Router();
|
|
5
|
-
const run = (handler) => (req, res) => {
|
|
5
|
+
const run = (handler) => async (req, res) => {
|
|
6
6
|
try {
|
|
7
7
|
const sessionId = resolveSession((req.method === 'GET' ? req.query : req.body) ?? {});
|
|
8
8
|
if (!sessionId)
|
|
9
9
|
throw new CollaborationError('SESSION_NOT_FOUND', 'Run inside a Termdock managed session', 404);
|
|
10
|
-
handler(req, res, sessionId);
|
|
10
|
+
await handler(req, res, sessionId);
|
|
11
11
|
}
|
|
12
12
|
catch (error) {
|
|
13
13
|
res.status(error instanceof CollaborationError ? error.httpStatus : 400).json({ ok: false,
|
|
14
14
|
code: error instanceof CollaborationError ? error.code : 'COLLABORATION_ERROR', error: error instanceof Error ? error.message : String(error) });
|
|
15
15
|
}
|
|
16
16
|
};
|
|
17
|
+
router.post('/route/rebind', run(async (req, res, sessionId) => {
|
|
18
|
+
if (!rebind)
|
|
19
|
+
throw new CollaborationError('ROUTE_REBIND_UNAVAILABLE', 'Route rebinding is not available');
|
|
20
|
+
const pane = req.body.pane;
|
|
21
|
+
if (pane !== undefined && pane !== null && (typeof pane !== 'string' || !/^%\d+$/.test(pane))) {
|
|
22
|
+
throw new CollaborationError('INVALID_PANE', 'pane must be a tmux pane id such as %3');
|
|
23
|
+
}
|
|
24
|
+
res.json({ ok: true, route: await rebind(sessionId, pane ?? null) });
|
|
25
|
+
}));
|
|
17
26
|
const ownMessage = (id, sessionId, recipientOnly = false) => {
|
|
18
27
|
const message = store.getMessage(id);
|
|
19
28
|
if (!message || (message.toSessionId !== sessionId && (recipientOnly || message.fromSessionId !== sessionId)))
|
|
@@ -27,11 +36,13 @@ export function collaborationRoutes({ store, resolveSession, deliver }) {
|
|
|
27
36
|
};
|
|
28
37
|
router.get('/capabilities', run((_req, res) => {
|
|
29
38
|
res.json({ protocol_version: 2, limits: COLLAB_LIMITS,
|
|
39
|
+
routing: { background_recovery: true, explicit_rebind: Boolean(rebind), fixed_tmux_pane: true },
|
|
30
40
|
statuses: ['pending', 'delivered', 'read', 'failed', 'expired'], queued_status: 'pending',
|
|
31
41
|
semantics: { delivered: 'written to terminal; no application acknowledgement implied', read: 'explicit consumer acknowledgement',
|
|
32
42
|
ack: 'explicit response_kind=ack', result: 'explicit response_kind=result; inspect task.status and evidence',
|
|
33
43
|
done: 'adapter reports turn ended, never proof of task completion', heartbeat: 'null unless explicitly observed',
|
|
34
|
-
timeout: 'stops waiting, does not cancel delivery'
|
|
44
|
+
timeout: 'stops waiting, does not cancel delivery',
|
|
45
|
+
retry: 'same message id may be submitted again after a crash or uncertain transport result; consumers deduplicate by message id' } });
|
|
35
46
|
}));
|
|
36
47
|
router.post('/send', run((req, res, sessionId) => {
|
|
37
48
|
const target = typeof req.body.targetSessionId === 'string' ? req.body.targetSessionId.trim() : '';
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export class CollaborationRoutingStore {
|
|
4
|
+
file;
|
|
5
|
+
bindings = new Map();
|
|
6
|
+
loadError = null;
|
|
7
|
+
constructor(file) {
|
|
8
|
+
this.file = file;
|
|
9
|
+
try {
|
|
10
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
11
|
+
if (data.version !== 1 || !Array.isArray(data.bindings))
|
|
12
|
+
throw new Error('Invalid collaboration routing store');
|
|
13
|
+
for (const binding of data.bindings) {
|
|
14
|
+
if (!binding || typeof binding.sessionId !== 'string' || !['shell', 'tmux'].includes(binding.mode)
|
|
15
|
+
|| ![binding.backendSessionId, binding.tmuxSessionName, binding.agentSlug, binding.nativeSessionId].every((value) => value === null || typeof value === 'string')
|
|
16
|
+
|| (binding.pane !== null && (!binding.pane || !Number.isInteger(binding.pane.serverPid) || !Number.isInteger(binding.pane.panePid)
|
|
17
|
+
|| !/^%\d+$/.test(binding.pane.paneId) || !/^\$\d+$/.test(binding.pane.sessionId) || typeof binding.pane.agentSlug !== 'string'))) {
|
|
18
|
+
throw new Error('Invalid collaboration binding');
|
|
19
|
+
}
|
|
20
|
+
this.bindings.set(binding.sessionId, binding);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error.code !== 'ENOENT') {
|
|
25
|
+
this.bindings.clear();
|
|
26
|
+
this.loadError = error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
get(id) {
|
|
31
|
+
const binding = this.bindings.get(id);
|
|
32
|
+
return binding ? structuredClone(binding) : null;
|
|
33
|
+
}
|
|
34
|
+
assertReadable() {
|
|
35
|
+
if (this.loadError)
|
|
36
|
+
throw new Error('COLLABORATION_ROUTING_STORE_UNREADABLE', { cause: this.loadError });
|
|
37
|
+
}
|
|
38
|
+
ownerOfBackend(id) {
|
|
39
|
+
return [...this.bindings.values()].find((binding) => binding.backendSessionId === id)?.sessionId ?? null;
|
|
40
|
+
}
|
|
41
|
+
bind(binding) {
|
|
42
|
+
this.assertReadable();
|
|
43
|
+
const owner = binding.backendSessionId ? this.ownerOfBackend(binding.backendSessionId) : null;
|
|
44
|
+
if (owner && owner !== binding.sessionId)
|
|
45
|
+
throw new Error('BACKEND_ALREADY_BOUND');
|
|
46
|
+
if (JSON.stringify(this.bindings.get(binding.sessionId)) === JSON.stringify(binding))
|
|
47
|
+
return;
|
|
48
|
+
const next = new Map(this.bindings);
|
|
49
|
+
next.set(binding.sessionId, structuredClone(binding));
|
|
50
|
+
this.persist(next);
|
|
51
|
+
}
|
|
52
|
+
remove(id) {
|
|
53
|
+
if (!this.bindings.has(id))
|
|
54
|
+
return;
|
|
55
|
+
const next = new Map(this.bindings);
|
|
56
|
+
next.delete(id);
|
|
57
|
+
this.persist(next);
|
|
58
|
+
}
|
|
59
|
+
persist(next) {
|
|
60
|
+
this.assertReadable();
|
|
61
|
+
fs.mkdirSync(path.dirname(this.file), { recursive: true });
|
|
62
|
+
const temporary = `${this.file}.${process.pid}.tmp`;
|
|
63
|
+
fs.writeFileSync(temporary, JSON.stringify({ version: 1, bindings: [...next.values()] }), { mode: 0o600 });
|
|
64
|
+
fs.renameSync(temporary, this.file);
|
|
65
|
+
this.bindings = next;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Once pinned, changing the active pane cannot change the recipient. A reused
|
|
69
|
+
// tmux name/pane after a server restart must not inherit an old peer's inbox.
|
|
70
|
+
export function selectCollaborationPane(binding, panes) {
|
|
71
|
+
if (binding.pane) {
|
|
72
|
+
const previous = binding.pane;
|
|
73
|
+
const pane = panes.find((candidate) => candidate.serverPid === previous.serverPid
|
|
74
|
+
&& candidate.sessionId === previous.sessionId && candidate.paneId === previous.paneId && candidate.panePid === previous.panePid);
|
|
75
|
+
if (!pane)
|
|
76
|
+
return { state: 'identity-mismatch', reason: 'TMUX_PANE_CHANGED' };
|
|
77
|
+
if (!pane.agentSlug)
|
|
78
|
+
return { state: 'agent-exited', reason: 'AGENT_NOT_RUNNING' };
|
|
79
|
+
if (pane.agentSlug !== previous.agentSlug || (previous.nativeSessionId && pane.nativeSessionId && previous.nativeSessionId !== pane.nativeSessionId)) {
|
|
80
|
+
return { state: 'identity-mismatch', reason: 'AGENT_IDENTITY_CHANGED' };
|
|
81
|
+
}
|
|
82
|
+
return { state: 'ready', pane };
|
|
83
|
+
}
|
|
84
|
+
const candidates = panes.filter((pane) => pane.agentSlug && (!binding.agentSlug || binding.agentSlug === pane.agentSlug)
|
|
85
|
+
&& (!binding.nativeSessionId || !pane.nativeSessionId || binding.nativeSessionId === pane.nativeSessionId));
|
|
86
|
+
const exact = binding.nativeSessionId ? candidates.filter((pane) => pane.nativeSessionId === binding.nativeSessionId) : [];
|
|
87
|
+
const matches = exact.length ? exact : candidates;
|
|
88
|
+
if (matches.length === 1)
|
|
89
|
+
return { state: 'ready', pane: matches[0] };
|
|
90
|
+
if (!matches.length && panes.some((pane) => pane.agentSlug))
|
|
91
|
+
return { state: 'identity-mismatch', reason: 'AGENT_IDENTITY_CHANGED' };
|
|
92
|
+
return matches.length > 1 ? { state: 'ambiguous', reason: 'MULTIPLE_AGENT_PANES' }
|
|
93
|
+
: { state: 'agent-exited', reason: 'AGENT_NOT_RUNNING_OR_IDENTITY_CHANGED' };
|
|
94
|
+
}
|
|
@@ -160,6 +160,10 @@ export class CollaborationStore {
|
|
|
160
160
|
this.expire();
|
|
161
161
|
return this.document.messages.filter((message) => message.toSessionId === sessionId && message.status === 'pending').length;
|
|
162
162
|
}
|
|
163
|
+
pendingRecipients() {
|
|
164
|
+
this.expire();
|
|
165
|
+
return [...new Set(this.document.messages.filter((message) => message.status === 'pending').map((message) => message.toSessionId))];
|
|
166
|
+
}
|
|
163
167
|
markDelivered(messageIds) {
|
|
164
168
|
return this.updateStatus(messageIds, 'delivered');
|
|
165
169
|
}
|
|
@@ -249,7 +253,8 @@ export class CollaborationStore {
|
|
|
249
253
|
expire() {
|
|
250
254
|
let changed = false;
|
|
251
255
|
for (const message of this.document.messages)
|
|
252
|
-
if (message.status === 'pending' && message.expiresAt && message.expiresAt <= Date.now()
|
|
256
|
+
if (message.status === 'pending' && message.expiresAt && message.expiresAt <= Date.now()
|
|
257
|
+
&& this.document.transport?.[message.id]?.last_error !== 'DELIVERY_IN_PROGRESS') {
|
|
253
258
|
message.status = 'expired';
|
|
254
259
|
message.failureReason = 'MESSAGE_EXPIRED';
|
|
255
260
|
changed = true;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { buildBracketedSubmitBytes } from './promptDelivery.js';
|
|
2
|
+
export async function writeCollaborationTmuxPane(run, pane, prompt) {
|
|
3
|
+
const identity = (await run(['display-message', '-p', '-t', pane.paneId,
|
|
4
|
+
'#{pid}:#{session_id}:#{pane_id}:#{pane_pid}'])).trim();
|
|
5
|
+
if (identity !== `${pane.serverPid}:${pane.sessionId}:${pane.paneId}:${pane.panePid}`)
|
|
6
|
+
throw new Error('TMUX_PANE_CHANGED');
|
|
7
|
+
// A fixed pane target is independent of the current window, keyboard focus
|
|
8
|
+
// and browser presence. One invocation preserves the bracketed submit bytes.
|
|
9
|
+
await run(['send-keys', '-t', pane.paneId, '-l', '--', buildBracketedSubmitBytes(prompt)]);
|
|
10
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export function resolveCollaborationBackend(record, records, backends, routing) {
|
|
2
|
+
routing.assertReadable();
|
|
3
|
+
const binding = routing.get(record.sessionId);
|
|
4
|
+
const target = binding ? { ...record, mode: binding.mode, tmuxSessionName: binding.tmuxSessionName,
|
|
5
|
+
backendSessionId: binding.backendSessionId,
|
|
6
|
+
agentResume: binding.agentSlug ? { slug: binding.agentSlug, sessionId: binding.nativeSessionId } : record.agentResume } : record;
|
|
7
|
+
const existing = target.backendSessionId ? backends.get(target.backendSessionId) : null;
|
|
8
|
+
const recovered = existing && existing.mode === target.mode && existing.tmuxSessionName === target.tmuxSessionName
|
|
9
|
+
? [target.backendSessionId, existing]
|
|
10
|
+
: recoverSessionBinding({ ...target, backendSessionId: null }, records, backends, binding);
|
|
11
|
+
if (!recovered)
|
|
12
|
+
return null;
|
|
13
|
+
const [id, backend] = recovered;
|
|
14
|
+
if (backend.mode !== 'shell' && backend.mode !== 'tmux')
|
|
15
|
+
return null;
|
|
16
|
+
const owner = routing.ownerOfBackend(id);
|
|
17
|
+
if (owner && owner !== record.sessionId)
|
|
18
|
+
return null;
|
|
19
|
+
if (records.some((other) => other.sessionId !== record.sessionId && other.backendSessionId === id))
|
|
20
|
+
return null;
|
|
21
|
+
routing.bind({ sessionId: record.sessionId, backendSessionId: id, mode: backend.mode,
|
|
22
|
+
tmuxSessionName: backend.tmuxSessionName,
|
|
23
|
+
agentSlug: binding?.agentSlug ?? backend.agent?.slug ?? record.agentResume?.slug ?? null,
|
|
24
|
+
nativeSessionId: binding?.nativeSessionId ?? backend.agentSession?.sessionId ?? record.agentResume?.sessionId ?? null,
|
|
25
|
+
pane: binding?.pane ?? null });
|
|
26
|
+
return recovered;
|
|
27
|
+
}
|
|
28
|
+
// Match live runtime evidence, never the search index or a title/cwd heuristic.
|
|
29
|
+
export function recoverSessionBinding(record, records, backends, binding) {
|
|
30
|
+
if (binding && binding.sessionId !== record.sessionId)
|
|
31
|
+
return null;
|
|
32
|
+
if (record.backendSessionId && backends.has(record.backendSessionId))
|
|
33
|
+
return null;
|
|
34
|
+
const nativeSessionId = binding?.nativeSessionId ?? record.agentResume?.sessionId;
|
|
35
|
+
const agentSlug = binding?.agentSlug ?? record.agentResume?.slug;
|
|
36
|
+
const candidates = [...backends].filter(([id, backend]) => {
|
|
37
|
+
if (records.some((other) => other.sessionId !== record.sessionId && other.backendSessionId === id))
|
|
38
|
+
return false;
|
|
39
|
+
if (backend.mode !== record.mode)
|
|
40
|
+
return false;
|
|
41
|
+
if (record.tmuxSessionName && backend.tmuxSessionName !== record.tmuxSessionName)
|
|
42
|
+
return false;
|
|
43
|
+
const nativeId = backend.agentSession?.sessionId;
|
|
44
|
+
const slug = backend.agent?.slug;
|
|
45
|
+
if (agentSlug && slug && agentSlug !== slug)
|
|
46
|
+
return false;
|
|
47
|
+
if (nativeSessionId && nativeId && nativeSessionId !== nativeId)
|
|
48
|
+
return false;
|
|
49
|
+
if (record.agentResume && slug && record.agentResume.slug !== slug)
|
|
50
|
+
return false;
|
|
51
|
+
if (record.agentResume?.sessionId && nativeId && record.agentResume.sessionId !== nativeId)
|
|
52
|
+
return false;
|
|
53
|
+
const tmuxMatch = record.mode === 'tmux' && Boolean(record.tmuxSessionName)
|
|
54
|
+
&& backend.tmuxSessionName === record.tmuxSessionName;
|
|
55
|
+
const nativeMatch = Boolean(nativeId) && nativeSessionId === nativeId && agentSlug === slug;
|
|
56
|
+
return tmuxMatch || nativeMatch;
|
|
57
|
+
});
|
|
58
|
+
return candidates.length === 1 ? candidates[0] : null;
|
|
59
|
+
}
|
package/dist/server/entry.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { desktopDirectTargets } from './federation/desktopTargets.js';
|
|
1
2
|
import { createOpenAccessRouter } from './federation/openAccess.js';
|
|
2
3
|
import { createPasswordLoginRouter } from './federation/passwordLoginRoutes.js';
|
|
3
4
|
import { apiAccessGate, isTrustedLocalRequest } from './utils/apiAccess.js';
|
|
@@ -404,27 +405,42 @@ export function startServer(options = {}) {
|
|
|
404
405
|
const { server, scheme } = createServerForApp(app, options);
|
|
405
406
|
let entrySubjectAllowed = (_subjectId) => false;
|
|
406
407
|
let routeInvitations;
|
|
407
|
-
|
|
408
|
+
let federationServiceId = '';
|
|
409
|
+
const desktopTargets = () => desktopDirectTargets(path.join(homedir(), '.termdock', 'desktop.json'), federationServiceId);
|
|
410
|
+
const routeAccess = new RouteAccess(path.join(homedir(), '.termdock', 'federation', 'routes.json'), Date.now, (subjectId, targetServiceId) => routeInvitations?.allows(subjectId, targetServiceId) === true, desktopTargets);
|
|
408
411
|
const relayRouter = new RelayRouter({
|
|
409
412
|
authenticate: context => context,
|
|
410
413
|
allowRegister: (principal, serviceId) => routeAccess.allowRegister(principal, serviceId),
|
|
411
414
|
allowRoute: (principal, serviceId) => routeAccess.allowRoute(principal, serviceId),
|
|
412
415
|
});
|
|
413
416
|
const dynamicDirectTargets = new Map();
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
417
|
+
const refreshDirectTargets = () => {
|
|
418
|
+
const desired = new Map(routeAccess.configuredDirectTargets().filter(target => target.serviceId !== federationServiceId).map(target => [target.serviceId, target]));
|
|
419
|
+
for (const [id, registered] of dynamicDirectTargets) {
|
|
420
|
+
const target = desired.get(id);
|
|
421
|
+
if (!target || registered.signature !== JSON.stringify([target.url, target.caPath, target.caFingerprint256])) {
|
|
422
|
+
registered.close();
|
|
423
|
+
dynamicDirectTargets.delete(id);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
for (const [id, target] of desired)
|
|
427
|
+
if (!dynamicDirectTargets.has(id)) {
|
|
428
|
+
try {
|
|
429
|
+
dynamicDirectTargets.set(id, { signature: JSON.stringify([target.url, target.caPath, target.caFingerprint256]), close: attachRegisteredDirectTargets(relayRouter, [target], path.join(homedir(), '.termdock', 'federation')) });
|
|
430
|
+
}
|
|
431
|
+
catch { /* A malformed or unavailable local route remains unusable. */ }
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
refreshDirectTargets();
|
|
435
|
+
const desktopRoutesTimer = setInterval(refreshDirectTargets, 2000);
|
|
436
|
+
desktopRoutesTimer.unref();
|
|
437
|
+
server.once('close', () => { clearInterval(desktopRoutesTimer); for (const target of dynamicDirectTargets.values())
|
|
438
|
+
target.close(); });
|
|
423
439
|
server.once('close', () => relayRouter.close());
|
|
424
440
|
const federation = createFederationRuntime(app, path.join(homedir(), '.termdock', 'federation'), {
|
|
425
441
|
terminal: handleTerminalWebSocket, control: handleControlWebSocket,
|
|
426
442
|
}, {
|
|
427
|
-
listRouteTargets: () => routeAccess.configuredTargets().map(target => ({ ...target, available: relayRouter.hasRoute(target.serviceId) })),
|
|
443
|
+
listRouteTargets: () => { refreshDirectTargets(); return routeAccess.configuredTargets().map(target => ({ ...target, available: relayRouter.hasRoute(target.serviceId) })); },
|
|
428
444
|
listRouteAccess: () => routeInvitations?.list() || [],
|
|
429
445
|
grantRouteAccess: async (issuerId, targetServiceId, subjectId, url) => {
|
|
430
446
|
if (!routeInvitations)
|
|
@@ -438,8 +454,7 @@ export function startServer(options = {}) {
|
|
|
438
454
|
if (!runtime.store.authorize({ subjectId: issuerId, serviceId: runtime.serviceId, action: 'authorization.manage' }).allowed)
|
|
439
455
|
throw new Error('AUTHORIZATION_DENIED');
|
|
440
456
|
routeAccess.addDirectTarget(target);
|
|
441
|
-
|
|
442
|
-
dynamicDirectTargets.set(targetServiceId, attachRegisteredDirectTargets(relayRouter, [target]));
|
|
457
|
+
refreshDirectTargets();
|
|
443
458
|
}
|
|
444
459
|
return routeInvitations.grant(issuerId, targetServiceId, subjectId);
|
|
445
460
|
},
|
|
@@ -458,6 +473,8 @@ export function startServer(options = {}) {
|
|
|
458
473
|
},
|
|
459
474
|
});
|
|
460
475
|
void federation.then(runtime => {
|
|
476
|
+
federationServiceId = runtime.serviceId;
|
|
477
|
+
refreshDirectTargets();
|
|
461
478
|
app.locals.passwordRuntime = runtime;
|
|
462
479
|
entrySubjectAllowed = subjectId => runtime.store.authorize({ subjectId, serviceId: runtime.serviceId, action: 'authorization.manage' }).allowed;
|
|
463
480
|
routeInvitations = new RouteInvitationStore({
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
2
|
+
const peer = (value) => typeof value === 'string' && /^[a-zA-Z0-9_.:-]{1,160}$/.test(value);
|
|
3
|
+
const fingerprint = (value) => typeof value === 'string' && /^(?:[\da-f]{2}:){31}[\da-f]{2}$/i.test(value);
|
|
4
|
+
function origin(value) {
|
|
5
|
+
if (typeof value !== 'string' || value.length > 2048)
|
|
6
|
+
return;
|
|
7
|
+
try {
|
|
8
|
+
const url = new URL(value);
|
|
9
|
+
if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash || url.pathname !== '/')
|
|
10
|
+
return;
|
|
11
|
+
return url.origin;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Read only this OS user's Desktop directory. The local service must itself
|
|
18
|
+
* have been pinned there before its administrator can discover other entries.
|
|
19
|
+
* This supplies addresses, never the Desktop client's device authorization.
|
|
20
|
+
*/
|
|
21
|
+
export function desktopDirectTargets(file, localServiceId) {
|
|
22
|
+
if (!localServiceId)
|
|
23
|
+
return [];
|
|
24
|
+
try {
|
|
25
|
+
if (statSync(file).size > 1024 * 1024)
|
|
26
|
+
return [];
|
|
27
|
+
const data = JSON.parse(readFileSync(file, 'utf8'));
|
|
28
|
+
if (data?.version !== 1 || !Array.isArray(data.connections) || data.connections.length > 256)
|
|
29
|
+
return [];
|
|
30
|
+
const removed = new Set(Array.isArray(data.removedServiceKeys) ? data.removedServiceKeys : []);
|
|
31
|
+
const connections = data.connections.filter((item) => item && ![item.id, item.targetPeerId, item.serviceOrigin || item.url].some(key => removed.has(key)));
|
|
32
|
+
if (!connections.some((item) => item.targetPeerId === localServiceId))
|
|
33
|
+
return [];
|
|
34
|
+
const trusts = new Map();
|
|
35
|
+
if (Array.isArray(data.trustedCertificateAuthorities))
|
|
36
|
+
for (const trust of data.trustedCertificateAuthorities.slice(0, 256)) {
|
|
37
|
+
const url = origin(trust?.origin);
|
|
38
|
+
if (url && fingerprint(trust.fingerprint256))
|
|
39
|
+
trusts.set(url, trust.fingerprint256.toUpperCase());
|
|
40
|
+
}
|
|
41
|
+
const targets = new Map();
|
|
42
|
+
for (const item of connections) {
|
|
43
|
+
if (!peer(item.targetPeerId) || item.targetPeerId === localServiceId)
|
|
44
|
+
continue;
|
|
45
|
+
const url = origin(item.serviceOrigin || item.url);
|
|
46
|
+
if (!url)
|
|
47
|
+
continue;
|
|
48
|
+
// An entry-only bookmark does not supply a direct address for its target.
|
|
49
|
+
if ((!item.serviceOrigin && item.entryServiceId && item.entryServiceId !== item.targetPeerId)
|
|
50
|
+
|| (Array.isArray(item.routes) && item.routes.some((route) => route && route.targetPeerId !== item.targetPeerId && origin(route.url) === url)))
|
|
51
|
+
continue;
|
|
52
|
+
const caFingerprint256 = trusts.get(url);
|
|
53
|
+
targets.set(item.targetPeerId, { serviceId: item.targetPeerId, url,
|
|
54
|
+
...(typeof item.label === 'string' && item.label.trim() ? { label: item.label.trim().slice(0, 120) } : {}),
|
|
55
|
+
...(caFingerprint256 ? { caFingerprint256 } : {}) });
|
|
56
|
+
if (targets.size >= 64)
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
return [...targets.values()];
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import { readPinnedCertificateAuthority } from './trustedCa.js';
|
|
1
2
|
import { createIdentity, secureConnection } from './secureProtocol.js';
|
|
2
3
|
import { socketDuplex } from './socketDuplex.js';
|
|
3
4
|
import { readFileSync } from 'node:fs';
|
|
4
5
|
import { resolve } from 'node:path';
|
|
5
6
|
import WebSocket from 'ws';
|
|
6
7
|
function endpoint(config) {
|
|
7
|
-
if (!config || typeof config !== 'object' || Object.keys(config).some(key => !['serviceId', 'url', 'caPath'].includes(key)) || !/^[a-zA-Z0-9_.:-]{1,160}$/.test(config.serviceId) || typeof config.url !== 'string' || (config.caPath !== undefined && typeof config.caPath !== 'string'))
|
|
8
|
+
if (!config || typeof config !== 'object' || Object.keys(config).some(key => !['serviceId', 'url', 'caPath', 'caFingerprint256', 'label'].includes(key)) || !/^[a-zA-Z0-9_.:-]{1,160}$/.test(config.serviceId) || typeof config.url !== 'string' || (config.caPath !== undefined && typeof config.caPath !== 'string'))
|
|
8
9
|
throw new Error('Invalid direct target configuration');
|
|
10
|
+
if ((config.caFingerprint256 !== undefined && (typeof config.caFingerprint256 !== 'string' || !/^(?:[\da-f]{2}:){31}[\da-f]{2}$/i.test(config.caFingerprint256))) || (config.caPath && config.caFingerprint256) || (config.label !== undefined && (typeof config.label !== 'string' || config.label.length > 120)))
|
|
11
|
+
throw new Error('Invalid direct target metadata');
|
|
9
12
|
const url = new URL(config.url);
|
|
10
13
|
if (!['https:', 'wss:'].includes(url.protocol) || url.username || url.password || url.search || url.hash || !['/', '/api/federation/secure'].includes(url.pathname))
|
|
11
14
|
throw new Error('Direct targets require a verified HTTPS/WSS origin');
|
|
@@ -24,7 +27,7 @@ export function attachRegisteredDirectTargets(router, configs, baseDir = process
|
|
|
24
27
|
if (ids.has(config.serviceId))
|
|
25
28
|
throw new Error('Duplicate direct target');
|
|
26
29
|
ids.add(config.serviceId);
|
|
27
|
-
return { serviceId: config.serviceId, url, ca: config.caPath ? readFileSync(resolve(baseDir, config.caPath)) : undefined };
|
|
30
|
+
return { serviceId: config.serviceId, url, caFingerprint256: config.caFingerprint256, ca: config.caPath ? readFileSync(resolve(baseDir, config.caPath)) : undefined };
|
|
28
31
|
});
|
|
29
32
|
let stopped = false;
|
|
30
33
|
const sockets = new Set();
|
|
@@ -34,22 +37,25 @@ export function attachRegisteredDirectTargets(router, configs, baseDir = process
|
|
|
34
37
|
ws.terminate(); };
|
|
35
38
|
try {
|
|
36
39
|
for (const target of targets)
|
|
37
|
-
cleanups.push(router.registerDirect(target.serviceId,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
failed
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
40
|
+
cleanups.push(router.registerDirect(target.serviceId, async () => {
|
|
41
|
+
const ca = target.caFingerprint256 ? await readPinnedCertificateAuthority(target.url.replace(/^wss:/, 'https:'), target.caFingerprint256) : target.ca;
|
|
42
|
+
return new Promise((accept, reject) => {
|
|
43
|
+
if (stopped) {
|
|
44
|
+
reject(new Error('Direct target stopped'));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const ws = new WebSocket(target.url, { ca, rejectUnauthorized: true, followRedirects: false, handshakeTimeout: 10000, maxPayload: 256 * 1024 });
|
|
48
|
+
sockets.add(ws);
|
|
49
|
+
ws.once('close', () => sockets.delete(ws));
|
|
50
|
+
const failed = () => { reject(new Error('Direct target unavailable')); ws.terminate(); };
|
|
51
|
+
ws.once('error', failed);
|
|
52
|
+
ws.once('close', failed);
|
|
53
|
+
ws.once('open', () => { ws.off('error', failed); ws.off('close', failed); ws.on('error', () => ws.terminate()); if (stopped)
|
|
54
|
+
failed();
|
|
55
|
+
else
|
|
56
|
+
accept(ws); });
|
|
57
|
+
});
|
|
58
|
+
}));
|
|
53
59
|
}
|
|
54
60
|
catch (error) {
|
|
55
61
|
cleanup();
|
|
@@ -60,7 +66,9 @@ export function attachRegisteredDirectTargets(router, configs, baseDir = process
|
|
|
60
66
|
/** An administrator enabling a new backup first proves the target's pinned
|
|
61
67
|
* identity; HTTPS validation remains enabled throughout. */
|
|
62
68
|
export async function verifyDirectTarget(config) {
|
|
63
|
-
const
|
|
69
|
+
const url = endpoint(config);
|
|
70
|
+
const ca = config.caFingerprint256 ? await readPinnedCertificateAuthority(url.replace(/^wss:/, 'https:'), config.caFingerprint256) : config.caPath ? readFileSync(config.caPath) : undefined;
|
|
71
|
+
const ws = new WebSocket(url, { ca, rejectUnauthorized: true, followRedirects: false, handshakeTimeout: 5000, maxPayload: 256 * 1024 });
|
|
64
72
|
const duplex = socketDuplex(ws);
|
|
65
73
|
try {
|
|
66
74
|
await new Promise((resolve, reject) => { ws.once('open', resolve); ws.once('error', reject); ws.once('close', () => reject(new Error('Target closed'))); });
|