termdock 1.4.177 → 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-C8XqNgqI.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-D82AsWLQ.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-CJ-Nbvx2.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-EhGz7WYT.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-CSU_xQ0p.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-CMpJkAVR.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-DdwU8GCT.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-DneECPDp.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-B-1FVK7Q.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-D-wzoa3W.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-Bxpb_WMR.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-D71_qc39.js → c4Diagram-7LVT6UL2-CcRdiWo7.js} +1 -1
- package/dist/client/assets/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-DaoA2fdB.js → chunk-2Q5K7J3B-DJ_qzvZZ.js} +1 -1
- package/dist/client/assets/{chunk-5VM5RSS4-DJE3F6KX.js → chunk-5VM5RSS4-l3H0C2ci.js} +1 -1
- package/dist/client/assets/{chunk-F27PBJKO-Fo6_Yfem.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-OvvPIKNR.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-xDwFINP9.js → chunk-JWPE2WC7-BlXUS79m.js} +1 -1
- package/dist/client/assets/{chunk-POPQ4Y6H-CcYREIwJ.js → chunk-POPQ4Y6H-CU38G8hb.js} +1 -1
- package/dist/client/assets/{chunk-SVP7TREG-RoyEQIZj.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-GKJU5wA8.js → chunk-TICWLB2K-DrOYmcBB.js} +1 -1
- package/dist/client/assets/chunk-TICWLB2K-DrOYmcBB.js.br +0 -0
- package/dist/client/assets/chunk-TICWLB2K-DrOYmcBB.js.gz +0 -0
- package/dist/client/assets/{chunk-XXDRQBXY-CtZCuU2q.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-021juJwD.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-CD-YnLit.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-Co-hwYoH.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-ChSWkpnp.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-BpmYuBNX.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-Cx-_gytc.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-Cg5K3FpQ.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-GLqXwu1z.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-EFVmF_e3.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-D-sPYDKp.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-DqQolUqi.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-D73BOmvb.js → flowDiagram-HODETNUW-CpfW88Uc.js} +1 -1
- package/dist/client/assets/flowDiagram-HODETNUW-CpfW88Uc.js.br +0 -0
- package/dist/client/assets/{flowDiagram-HODETNUW-D73BOmvb.js.gz → flowDiagram-HODETNUW-CpfW88Uc.js.gz} +0 -0
- package/dist/client/assets/{ganttDiagram-EL5Y4UJY-B6O_nNIU.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-DdaGucbq.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-CzdxfpE1.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-BtHzVo8X.js → infoDiagram-27XIBGKW-D6sSgR2Y.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-5VMMS53U-DgiXYMer.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-BYofIzlG.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-84ncCLIW.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-CLVsAvDM.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-B12Qt26p.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-DrqTIbZK.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-B9nsO_NB.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-3o-3HkST.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-SY7QRxOR.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-CyEE_PWG.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-B23ZEt7E.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-0Q0Kx13M.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-kZaY3jCk.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-CrU3f8AK.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-7OcxFUfS.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-CrHGwgDV.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-DNTqdeW-.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-DorZejhA.js → sparkles-gqiuBkfP.js} +1 -1
- package/dist/client/assets/{stateDiagram-D77RDMKH-DiYeobqG.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-BeSh-Zg2.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-DqFz-84s.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-CftT9nTu.js → triangle-alert-DrBuMUPw.js} +1 -1
- package/dist/client/assets/{vennDiagram-4TSXK5OY-BFsIcqC-.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-B03VPv0r.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-BWleefr3.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 +33 -14
- package/dist/server/federation/desktopTargets.js +64 -0
- package/dist/server/federation/directRoutes.js +27 -19
- package/dist/server/federation/routeAccess.js +25 -5
- package/dist/server/federation/runtime.js +6 -0
- 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-CLI40MbV.js +0 -150
- package/dist/client/assets/App-CLI40MbV.js.br +0 -0
- package/dist/client/assets/App-CLI40MbV.js.gz +0 -0
- package/dist/client/assets/ChangeWalkthroughPanel-C8XqNgqI.js.br +0 -0
- package/dist/client/assets/ChangeWalkthroughPanel-C8XqNgqI.js.gz +0 -0
- package/dist/client/assets/DagPlayground-D82AsWLQ.js.br +0 -0
- package/dist/client/assets/DagPlayground-D82AsWLQ.js.gz +0 -0
- package/dist/client/assets/DiffLab-CJ-Nbvx2.js.br +0 -0
- package/dist/client/assets/DiffLab-CJ-Nbvx2.js.gz +0 -0
- package/dist/client/assets/DiffReview-EhGz7WYT.js.br +0 -0
- package/dist/client/assets/DiffReview-EhGz7WYT.js.gz +0 -0
- package/dist/client/assets/DiffReviewLab-CSU_xQ0p.js.br +0 -0
- package/dist/client/assets/DiffReviewLab-CSU_xQ0p.js.gz +0 -0
- package/dist/client/assets/DiffViewer-CMpJkAVR.js.br +0 -0
- package/dist/client/assets/DiffViewer-CMpJkAVR.js.gz +0 -0
- package/dist/client/assets/ModelPreview-DdwU8GCT.js.br +0 -0
- package/dist/client/assets/ModelPreview-DdwU8GCT.js.gz +0 -0
- package/dist/client/assets/RightSidebar-pet_AsxC.js +0 -79
- package/dist/client/assets/RightSidebar-pet_AsxC.js.br +0 -0
- package/dist/client/assets/RightSidebar-pet_AsxC.js.gz +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-DneECPDp.js.br +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-DneECPDp.js.gz +0 -0
- package/dist/client/assets/app-BGjVINzR.js +0 -34
- package/dist/client/assets/app-BGjVINzR.js.br +0 -0
- package/dist/client/assets/app-BGjVINzR.js.gz +0 -0
- package/dist/client/assets/arc-B-1FVK7Q.js.br +0 -0
- package/dist/client/assets/arc-B-1FVK7Q.js.gz +0 -0
- package/dist/client/assets/architectureDiagram-5GKGNRK7-D-wzoa3W.js.br +0 -0
- package/dist/client/assets/architectureDiagram-5GKGNRK7-D-wzoa3W.js.gz +0 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-Bxpb_WMR.js.br +0 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-Bxpb_WMR.js.gz +0 -0
- package/dist/client/assets/c4Diagram-7LVT6UL2-D71_qc39.js.br +0 -0
- package/dist/client/assets/c4Diagram-7LVT6UL2-D71_qc39.js.gz +0 -0
- package/dist/client/assets/channel-ClyE-cu4.js +0 -1
- package/dist/client/assets/chunk-F27PBJKO-Fo6_Yfem.js.br +0 -0
- package/dist/client/assets/chunk-F27PBJKO-Fo6_Yfem.js.gz +0 -0
- package/dist/client/assets/chunk-IMKFNOWR-OvvPIKNR.js.br +0 -0
- package/dist/client/assets/chunk-IMKFNOWR-OvvPIKNR.js.gz +0 -0
- package/dist/client/assets/chunk-SVP7TREG-RoyEQIZj.js.br +0 -0
- package/dist/client/assets/chunk-SVP7TREG-RoyEQIZj.js.gz +0 -0
- package/dist/client/assets/chunk-TICWLB2K-GKJU5wA8.js.br +0 -0
- package/dist/client/assets/chunk-TICWLB2K-GKJU5wA8.js.gz +0 -0
- package/dist/client/assets/classDiagram-ZZMXUADV-sqGtFMbp.js +0 -1
- package/dist/client/assets/classDiagram-v2-VYDZK3BY-sqGtFMbp.js +0 -1
- package/dist/client/assets/cose-bilkent-JH36ORCC-021juJwD.js.br +0 -0
- package/dist/client/assets/cose-bilkent-JH36ORCC-021juJwD.js.gz +0 -0
- package/dist/client/assets/cynefin-OW5HDTMX-CD-YnLit.js.br +0 -0
- package/dist/client/assets/cynefin-OW5HDTMX-CD-YnLit.js.gz +0 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-Co-hwYoH.js.br +0 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-Co-hwYoH.js.gz +0 -0
- package/dist/client/assets/dagre-GXQ25YYZ-ChSWkpnp.js.br +0 -0
- package/dist/client/assets/dagre-GXQ25YYZ-ChSWkpnp.js.gz +0 -0
- package/dist/client/assets/diagram-S7CK7UJ4-BpmYuBNX.js.br +0 -0
- package/dist/client/assets/diagram-S7CK7UJ4-BpmYuBNX.js.gz +0 -0
- package/dist/client/assets/diagram-UQ7AKVKN-Cx-_gytc.js.br +0 -0
- package/dist/client/assets/diagram-UQ7AKVKN-Cx-_gytc.js.gz +0 -0
- package/dist/client/assets/diagram-VSXAHHWV-Cg5K3FpQ.js.br +0 -0
- package/dist/client/assets/diagram-VSXAHHWV-Cg5K3FpQ.js.gz +0 -0
- package/dist/client/assets/diagram-VX7I27RA-GLqXwu1z.js.br +0 -0
- package/dist/client/assets/diagram-VX7I27RA-GLqXwu1z.js.gz +0 -0
- package/dist/client/assets/diagram-Z3DM3KII-EFVmF_e3.js.br +0 -0
- package/dist/client/assets/diagram-Z3DM3KII-EFVmF_e3.js.gz +0 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-D-sPYDKp.js.br +0 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-D-sPYDKp.js.gz +0 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-DqQolUqi.js.br +0 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-DqQolUqi.js.gz +0 -0
- package/dist/client/assets/flowDiagram-HODETNUW-D73BOmvb.js.br +0 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-B6O_nNIU.js.br +0 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-B6O_nNIU.js.gz +0 -0
- package/dist/client/assets/gestureArbiter-DdaGucbq.js.br +0 -0
- package/dist/client/assets/gestureArbiter-DdaGucbq.js.gz +0 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-CzdxfpE1.js.br +0 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-CzdxfpE1.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-DgiXYMer.js.br +0 -0
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-DgiXYMer.js.gz +0 -0
- package/dist/client/assets/journeyDiagram-3NMN7TZE-BYofIzlG.js.br +0 -0
- package/dist/client/assets/journeyDiagram-3NMN7TZE-BYofIzlG.js.gz +0 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-84ncCLIW.js.br +0 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-84ncCLIW.js.gz +0 -0
- package/dist/client/assets/layout-CLVsAvDM.js.br +0 -0
- package/dist/client/assets/layout-CLVsAvDM.js.gz +0 -0
- package/dist/client/assets/linear-B12Qt26p.js.br +0 -0
- package/dist/client/assets/linear-B12Qt26p.js.gz +0 -0
- package/dist/client/assets/mermaid.core-DrqTIbZK.js.br +0 -0
- package/dist/client/assets/mermaid.core-DrqTIbZK.js.gz +0 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-B9nsO_NB.js.br +0 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-B9nsO_NB.js.gz +0 -0
- package/dist/client/assets/passwordBootstrap-3o-3HkST.js.br +0 -0
- package/dist/client/assets/passwordBootstrap-3o-3HkST.js.gz +0 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-SY7QRxOR.js.br +0 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-SY7QRxOR.js.gz +0 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-CyEE_PWG.js.br +0 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-CyEE_PWG.js.gz +0 -0
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-B23ZEt7E.js.br +0 -0
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-B23ZEt7E.js.gz +0 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-0Q0Kx13M.js.br +0 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-0Q0Kx13M.js.gz +0 -0
- package/dist/client/assets/referenceLongPress-kZaY3jCk.js.br +0 -0
- package/dist/client/assets/referenceLongPress-kZaY3jCk.js.gz +0 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-CrU3f8AK.js.br +0 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-CrU3f8AK.js.gz +0 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-7OcxFUfS.js.br +0 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-7OcxFUfS.js.gz +0 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-CrHGwgDV.js.br +0 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-CrHGwgDV.js.gz +0 -0
- package/dist/client/assets/sizeCapture-INFHLROL-DNTqdeW-.js.br +0 -0
- package/dist/client/assets/sizeCapture-INFHLROL-DNTqdeW-.js.gz +0 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-DiYeobqG.js.br +0 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-DiYeobqG.js.gz +0 -0
- package/dist/client/assets/stateDiagram-v2-MP3YSRHH-BOF1IPOj.js +0 -1
- package/dist/client/assets/swimlanes-42K2YHIH-BeSh-Zg2.js.br +0 -0
- package/dist/client/assets/swimlanes-42K2YHIH-BeSh-Zg2.js.gz +0 -0
- package/dist/client/assets/swimlanesDiagram-VR7AAH4N-Bi6Gl65Z.js +0 -8
- package/dist/client/assets/timeline-definition-24CTP7MA-DqFz-84s.js.br +0 -0
- package/dist/client/assets/timeline-definition-24CTP7MA-DqFz-84s.js.gz +0 -0
- package/dist/client/assets/vennDiagram-4TSXK5OY-BFsIcqC-.js.br +0 -0
- package/dist/client/assets/vennDiagram-4TSXK5OY-BFsIcqC-.js.gz +0 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-B03VPv0r.js.br +0 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-B03VPv0r.js.gz +0 -0
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-BWleefr3.js.br +0 -0
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-BWleefr3.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,26 +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
|
}, {
|
|
443
|
+
listRouteTargets: () => { refreshDirectTargets(); return routeAccess.configuredTargets().map(target => ({ ...target, available: relayRouter.hasRoute(target.serviceId) })); },
|
|
427
444
|
listRouteAccess: () => routeInvitations?.list() || [],
|
|
428
445
|
grantRouteAccess: async (issuerId, targetServiceId, subjectId, url) => {
|
|
429
446
|
if (!routeInvitations)
|
|
@@ -437,8 +454,7 @@ export function startServer(options = {}) {
|
|
|
437
454
|
if (!runtime.store.authorize({ subjectId: issuerId, serviceId: runtime.serviceId, action: 'authorization.manage' }).allowed)
|
|
438
455
|
throw new Error('AUTHORIZATION_DENIED');
|
|
439
456
|
routeAccess.addDirectTarget(target);
|
|
440
|
-
|
|
441
|
-
dynamicDirectTargets.set(targetServiceId, attachRegisteredDirectTargets(relayRouter, [target]));
|
|
457
|
+
refreshDirectTargets();
|
|
442
458
|
}
|
|
443
459
|
return routeInvitations.grant(issuerId, targetServiceId, subjectId);
|
|
444
460
|
},
|
|
@@ -457,6 +473,8 @@ export function startServer(options = {}) {
|
|
|
457
473
|
},
|
|
458
474
|
});
|
|
459
475
|
void federation.then(runtime => {
|
|
476
|
+
federationServiceId = runtime.serviceId;
|
|
477
|
+
refreshDirectTargets();
|
|
460
478
|
app.locals.passwordRuntime = runtime;
|
|
461
479
|
entrySubjectAllowed = subjectId => runtime.store.authorize({ subjectId, serviceId: runtime.serviceId, action: 'authorization.manage' }).allowed;
|
|
462
480
|
routeInvitations = new RouteInvitationStore({
|
|
@@ -552,8 +570,9 @@ export function startServer(options = {}) {
|
|
|
552
570
|
}
|
|
553
571
|
const pathname = url.pathname;
|
|
554
572
|
if (pathname === '/api/federation/relay') {
|
|
555
|
-
//
|
|
556
|
-
|
|
573
|
+
// A paired device can use B from another HTTPS PWA origin. This channel
|
|
574
|
+
// ignores cookies: the single-use target-scoped ticket below is mandatory.
|
|
575
|
+
if (scheme !== 'https' || !secureChannelOriginAllowed(request.headers.origin, () => isUpgradeOriginAllowed(request.headers.origin, request.headers.host))) {
|
|
557
576
|
socket.end('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');
|
|
558
577
|
return;
|
|
559
578
|
}
|
|
@@ -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'))); });
|