termdock 1.4.231 → 1.4.232
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/.desktop-dist/main.js +9 -0
- package/dist/client/assets/{App-Bl-MnHlK.js → App-Di12Ej1O.js} +3 -3
- package/dist/client/assets/App-Di12Ej1O.js.br +0 -0
- package/dist/client/assets/App-Di12Ej1O.js.gz +0 -0
- package/dist/client/assets/{ChangeWalkthroughPanel-oVCcuJL3.js → ChangeWalkthroughPanel-pOBFJTxi.js} +1 -1
- package/dist/client/assets/ChangeWalkthroughPanel-pOBFJTxi.js.br +0 -0
- package/dist/client/assets/ChangeWalkthroughPanel-pOBFJTxi.js.gz +0 -0
- package/dist/client/assets/{DagPlayground-B33ZJSNC.js → DagPlayground-DWaLn2pa.js} +1 -1
- package/dist/client/assets/DagPlayground-DWaLn2pa.js.br +0 -0
- package/dist/client/assets/DagPlayground-DWaLn2pa.js.gz +0 -0
- package/dist/client/assets/{DiffLab-CZRP_SqH.js → DiffLab-DMvlScEP.js} +1 -1
- package/dist/client/assets/DiffLab-DMvlScEP.js.br +0 -0
- package/dist/client/assets/DiffLab-DMvlScEP.js.gz +0 -0
- package/dist/client/assets/{DiffReview-BheUk7E5.js → DiffReview-D1M9kVJy.js} +1 -1
- package/dist/client/assets/DiffReview-D1M9kVJy.js.br +0 -0
- package/dist/client/assets/DiffReview-D1M9kVJy.js.gz +0 -0
- package/dist/client/assets/{DiffReviewLab-DupHSbbd.js → DiffReviewLab-BVkhfOhb.js} +1 -1
- package/dist/client/assets/DiffReviewLab-BVkhfOhb.js.br +0 -0
- package/dist/client/assets/DiffReviewLab-BVkhfOhb.js.gz +0 -0
- package/dist/client/assets/{DiffViewer-Cd5EE8E9.js → DiffViewer-DN5Akyco.js} +1 -1
- package/dist/client/assets/DiffViewer-DN5Akyco.js.br +0 -0
- package/dist/client/assets/DiffViewer-DN5Akyco.js.gz +0 -0
- package/dist/client/assets/{ModelPreview-Fnuxnt8A.js → ModelPreview-D5Vl9PdY.js} +1 -1
- package/dist/client/assets/ModelPreview-D5Vl9PdY.js.br +0 -0
- package/dist/client/assets/ModelPreview-D5Vl9PdY.js.gz +0 -0
- package/dist/client/assets/{RightSidebar-DTTmdNhf.js → RightSidebar-CNICBrOL.js} +4 -4
- package/dist/client/assets/RightSidebar-CNICBrOL.js.br +0 -0
- package/dist/client/assets/RightSidebar-CNICBrOL.js.gz +0 -0
- package/dist/client/assets/{abnfDiagram-VCTEODGH-1oyW4gyC.js → abnfDiagram-VCTEODGH-Coq8shqQ.js} +1 -1
- package/dist/client/assets/abnfDiagram-VCTEODGH-Coq8shqQ.js.br +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-Coq8shqQ.js.gz +0 -0
- package/dist/client/assets/app-BNPa5Xzm.js +35 -0
- package/dist/client/assets/app-BNPa5Xzm.js.br +0 -0
- package/dist/client/assets/app-BNPa5Xzm.js.gz +0 -0
- package/dist/client/assets/{arc-e9P469h4.js → arc-D9F88oM8.js} +1 -1
- package/dist/client/assets/arc-D9F88oM8.js.br +0 -0
- package/dist/client/assets/arc-D9F88oM8.js.gz +0 -0
- package/dist/client/assets/{architectureDiagram-5GKGNRK7-CloNCKLm.js → architectureDiagram-5GKGNRK7-Ci04e9H2.js} +1 -1
- package/dist/client/assets/architectureDiagram-5GKGNRK7-Ci04e9H2.js.br +0 -0
- package/dist/client/assets/architectureDiagram-5GKGNRK7-Ci04e9H2.js.gz +0 -0
- package/dist/client/assets/{blockDiagram-I7D4REHJ-D3wH4hOJ.js → blockDiagram-I7D4REHJ-B0V-2mCO.js} +1 -1
- package/dist/client/assets/blockDiagram-I7D4REHJ-B0V-2mCO.js.br +0 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-B0V-2mCO.js.gz +0 -0
- package/dist/client/assets/{c4Diagram-7LVT6UL2-CX5tSHre.js → c4Diagram-7LVT6UL2-CgHFeNkR.js} +1 -1
- package/dist/client/assets/c4Diagram-7LVT6UL2-CgHFeNkR.js.br +0 -0
- package/dist/client/assets/c4Diagram-7LVT6UL2-CgHFeNkR.js.gz +0 -0
- package/dist/client/assets/channel-Cf-_AhxJ.js +1 -0
- package/dist/client/assets/{chunk-2Q5K7J3B-Weq7o_yW.js → chunk-2Q5K7J3B-C1R3c0d8.js} +1 -1
- package/dist/client/assets/{chunk-5VM5RSS4-B1O3hmJE.js → chunk-5VM5RSS4-USD4Kst0.js} +1 -1
- package/dist/client/assets/{chunk-F27PBJKO-BRYx-Dtz.js → chunk-F27PBJKO-D-1deMoU.js} +1 -1
- package/dist/client/assets/chunk-F27PBJKO-D-1deMoU.js.br +0 -0
- package/dist/client/assets/chunk-F27PBJKO-D-1deMoU.js.gz +0 -0
- package/dist/client/assets/{chunk-IMKFNOWR-z6lXRY7P.js → chunk-IMKFNOWR-DyunJ4J2.js} +1 -1
- package/dist/client/assets/chunk-IMKFNOWR-DyunJ4J2.js.br +0 -0
- package/dist/client/assets/chunk-IMKFNOWR-DyunJ4J2.js.gz +0 -0
- package/dist/client/assets/{chunk-JWPE2WC7-DdDHAlPc.js → chunk-JWPE2WC7-ClM1w-Bz.js} +1 -1
- package/dist/client/assets/{chunk-POPQ4Y6H-BYbFuciT.js → chunk-POPQ4Y6H-Dmz_lI83.js} +1 -1
- package/dist/client/assets/{chunk-SVP7TREG-Bk-X0NaV.js → chunk-SVP7TREG-JSkpe0Ru.js} +1 -1
- package/dist/client/assets/chunk-SVP7TREG-JSkpe0Ru.js.br +0 -0
- package/dist/client/assets/chunk-SVP7TREG-JSkpe0Ru.js.gz +0 -0
- package/dist/client/assets/{chunk-TICWLB2K-BjUqsphB.js → chunk-TICWLB2K-DNRVmHct.js} +1 -1
- package/dist/client/assets/chunk-TICWLB2K-DNRVmHct.js.br +0 -0
- package/dist/client/assets/chunk-TICWLB2K-DNRVmHct.js.gz +0 -0
- package/dist/client/assets/{chunk-XXDRQBXY-DSXlNway.js → chunk-XXDRQBXY-C_nQBcA8.js} +1 -1
- package/dist/client/assets/classDiagram-ZZMXUADV-CCnYK6Cj.js +1 -0
- package/dist/client/assets/classDiagram-v2-VYDZK3BY-CCnYK6Cj.js +1 -0
- package/dist/client/assets/{cose-bilkent-JH36ORCC-Bw8F9aUq.js → cose-bilkent-JH36ORCC-D2_1Vg5L.js} +1 -1
- package/dist/client/assets/cose-bilkent-JH36ORCC-D2_1Vg5L.js.br +0 -0
- package/dist/client/assets/cose-bilkent-JH36ORCC-D2_1Vg5L.js.gz +0 -0
- package/dist/client/assets/{cynefin-OW5HDTMX-C5oB4ZTA.js → cynefin-OW5HDTMX-C_zLtwow.js} +1 -1
- package/dist/client/assets/cynefin-OW5HDTMX-C_zLtwow.js.br +0 -0
- package/dist/client/assets/{cynefin-OW5HDTMX-C5oB4ZTA.js.gz → cynefin-OW5HDTMX-C_zLtwow.js.gz} +0 -0
- package/dist/client/assets/{cynefinDiagram-5FMLGOSQ-BkbwVdAD.js → cynefinDiagram-5FMLGOSQ-B0Wvfl0v.js} +1 -1
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-B0Wvfl0v.js.br +0 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-B0Wvfl0v.js.gz +0 -0
- package/dist/client/assets/{dagre-GXQ25YYZ-8Jt1so7H.js → dagre-GXQ25YYZ-D4dD95T3.js} +1 -1
- package/dist/client/assets/dagre-GXQ25YYZ-D4dD95T3.js.br +0 -0
- package/dist/client/assets/dagre-GXQ25YYZ-D4dD95T3.js.gz +0 -0
- package/dist/client/assets/{diagram-S7CK7UJ4-DyWUtoUO.js → diagram-S7CK7UJ4-2nnj1lep.js} +1 -1
- package/dist/client/assets/diagram-S7CK7UJ4-2nnj1lep.js.br +0 -0
- package/dist/client/assets/diagram-S7CK7UJ4-2nnj1lep.js.gz +0 -0
- package/dist/client/assets/{diagram-UQ7AKVKN-BBnqsz2l.js → diagram-UQ7AKVKN-C9N4zQ8r.js} +1 -1
- package/dist/client/assets/diagram-UQ7AKVKN-C9N4zQ8r.js.br +0 -0
- package/dist/client/assets/diagram-UQ7AKVKN-C9N4zQ8r.js.gz +0 -0
- package/dist/client/assets/{diagram-VSXAHHWV-DQ2JxOjW.js → diagram-VSXAHHWV-Cv7_lYe4.js} +1 -1
- package/dist/client/assets/diagram-VSXAHHWV-Cv7_lYe4.js.br +0 -0
- package/dist/client/assets/diagram-VSXAHHWV-Cv7_lYe4.js.gz +0 -0
- package/dist/client/assets/{diagram-VX7I27RA-Das3jLN9.js → diagram-VX7I27RA-BcgskUTT.js} +1 -1
- package/dist/client/assets/diagram-VX7I27RA-BcgskUTT.js.br +0 -0
- package/dist/client/assets/diagram-VX7I27RA-BcgskUTT.js.gz +0 -0
- package/dist/client/assets/{diagram-Z3DM3KII-0gbBMfrq.js → diagram-Z3DM3KII-v37kk7HI.js} +1 -1
- package/dist/client/assets/diagram-Z3DM3KII-v37kk7HI.js.br +0 -0
- package/dist/client/assets/diagram-Z3DM3KII-v37kk7HI.js.gz +0 -0
- package/dist/client/assets/{ebnfDiagram-PWID7BFC-DpAG2pFv.js → ebnfDiagram-PWID7BFC-CWRegGRp.js} +1 -1
- package/dist/client/assets/ebnfDiagram-PWID7BFC-CWRegGRp.js.br +0 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-CWRegGRp.js.gz +0 -0
- package/dist/client/assets/{erDiagram-RLTQ6QDP-x4B0lUNv.js → erDiagram-RLTQ6QDP-C-a8Ptae.js} +1 -1
- package/dist/client/assets/erDiagram-RLTQ6QDP-C-a8Ptae.js.br +0 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-C-a8Ptae.js.gz +0 -0
- package/dist/client/assets/{flowDiagram-HODETNUW-Dx34Jzbo.js → flowDiagram-HODETNUW-CZQBLCvO.js} +1 -1
- package/dist/client/assets/{flowDiagram-HODETNUW-Dx34Jzbo.js.br → flowDiagram-HODETNUW-CZQBLCvO.js.br} +0 -0
- package/dist/client/assets/flowDiagram-HODETNUW-CZQBLCvO.js.gz +0 -0
- package/dist/client/assets/{ganttDiagram-EL5Y4UJY-C7fWuwM6.js → ganttDiagram-EL5Y4UJY-CDJSVMvA.js} +1 -1
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-CDJSVMvA.js.br +0 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-CDJSVMvA.js.gz +0 -0
- package/dist/client/assets/{gestureArbiter-CwYdj3ki.js → gestureArbiter-3yU_eQSh.js} +1 -1
- package/dist/client/assets/gestureArbiter-3yU_eQSh.js.br +0 -0
- package/dist/client/assets/{gestureArbiter-CwYdj3ki.js.gz → gestureArbiter-3yU_eQSh.js.gz} +0 -0
- package/dist/client/assets/{gitGraphDiagram-WWUBYQGX-B6QlpyGY.js → gitGraphDiagram-WWUBYQGX-BpKnzvx5.js} +1 -1
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-BpKnzvx5.js.br +0 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-BpKnzvx5.js.gz +0 -0
- package/dist/client/assets/{infoDiagram-27XIBGKW-D-cyvLtB.js → infoDiagram-27XIBGKW-CvIbo5Yc.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-5VMMS53U-Bji-6SyT.js → ishikawaDiagram-5VMMS53U-iTcusz9p.js} +1 -1
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-iTcusz9p.js.br +0 -0
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-iTcusz9p.js.gz +0 -0
- package/dist/client/assets/{journeyDiagram-3NMN7TZE-FTfWDJ8K.js → journeyDiagram-3NMN7TZE-AnHrPTVQ.js} +1 -1
- package/dist/client/assets/journeyDiagram-3NMN7TZE-AnHrPTVQ.js.br +0 -0
- package/dist/client/assets/journeyDiagram-3NMN7TZE-AnHrPTVQ.js.gz +0 -0
- package/dist/client/assets/{kanban-definition-UXKFOSKX-CvrvLpNJ.js → kanban-definition-UXKFOSKX-BSGQKNkn.js} +1 -1
- package/dist/client/assets/kanban-definition-UXKFOSKX-BSGQKNkn.js.br +0 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-BSGQKNkn.js.gz +0 -0
- package/dist/client/assets/{layout-D04TPU52.js → layout-DWfvuKxK.js} +1 -1
- package/dist/client/assets/layout-DWfvuKxK.js.br +0 -0
- package/dist/client/assets/layout-DWfvuKxK.js.gz +0 -0
- package/dist/client/assets/{linear-tmc9NdkO.js → linear-YGUC9oFd.js} +1 -1
- package/dist/client/assets/linear-YGUC9oFd.js.br +0 -0
- package/dist/client/assets/linear-YGUC9oFd.js.gz +0 -0
- package/dist/client/assets/{mermaid.core-BghOeJM9.js → mermaid.core-C2ofdbDu.js} +5 -5
- package/dist/client/assets/mermaid.core-C2ofdbDu.js.br +0 -0
- package/dist/client/assets/mermaid.core-C2ofdbDu.js.gz +0 -0
- package/dist/client/assets/{mindmap-definition-YA3MSWOX-BEhTGgsF.js → mindmap-definition-YA3MSWOX-DhpmPHTj.js} +1 -1
- package/dist/client/assets/mindmap-definition-YA3MSWOX-DhpmPHTj.js.br +0 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-DhpmPHTj.js.gz +0 -0
- package/dist/client/assets/{passwordBootstrap-wvzKljMS.js → passwordBootstrap-VsBMR4A9.js} +1 -1
- package/dist/client/assets/passwordBootstrap-VsBMR4A9.js.br +0 -0
- package/dist/client/assets/passwordBootstrap-VsBMR4A9.js.gz +0 -0
- package/dist/client/assets/{pegDiagram-XKGWAZYB-BX1V7YDa.js → pegDiagram-XKGWAZYB-BRFYfMc1.js} +1 -1
- package/dist/client/assets/pegDiagram-XKGWAZYB-BRFYfMc1.js.br +0 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-BRFYfMc1.js.gz +0 -0
- package/dist/client/assets/{pieDiagram-E7YTZNPT-D5LMv0Qe.js → pieDiagram-E7YTZNPT-B3r-kmkG.js} +1 -1
- package/dist/client/assets/pieDiagram-E7YTZNPT-B3r-kmkG.js.br +0 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-B3r-kmkG.js.gz +0 -0
- package/dist/client/assets/{quadrantDiagram-AXDQQJYC-4fzegqtV.js → quadrantDiagram-AXDQQJYC-DR1I_zXC.js} +1 -1
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-DR1I_zXC.js.br +0 -0
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-DR1I_zXC.js.gz +0 -0
- package/dist/client/assets/{railroadDiagram-O6MQD6OU-bgwdyvOY.js → railroadDiagram-O6MQD6OU-Bvfl-CvM.js} +1 -1
- package/dist/client/assets/railroadDiagram-O6MQD6OU-Bvfl-CvM.js.br +0 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-Bvfl-CvM.js.gz +0 -0
- package/dist/client/assets/{referenceLongPress-Uw7_VECn.js → referenceLongPress-X-OUHgyO.js} +1 -1
- package/dist/client/assets/referenceLongPress-X-OUHgyO.js.br +0 -0
- package/dist/client/assets/referenceLongPress-X-OUHgyO.js.gz +0 -0
- package/dist/client/assets/{requirementDiagram-BXWQKSXE-CZhlA-YX.js → requirementDiagram-BXWQKSXE-C77Ip0mm.js} +1 -1
- package/dist/client/assets/requirementDiagram-BXWQKSXE-C77Ip0mm.js.br +0 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-C77Ip0mm.js.gz +0 -0
- package/dist/client/assets/{sankeyDiagram-P5KCCOFB-BSLsSYgS.js → sankeyDiagram-P5KCCOFB-C1irBgQp.js} +1 -1
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-C1irBgQp.js.br +0 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-C1irBgQp.js.gz +0 -0
- package/dist/client/assets/{sequenceDiagram-WJ2MYXX4-D5q4HIce.js → sequenceDiagram-WJ2MYXX4-ziivSu_M.js} +1 -1
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-ziivSu_M.js.br +0 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-ziivSu_M.js.gz +0 -0
- package/dist/client/assets/{sizeCapture-INFHLROL-C4i6fklz.js → sizeCapture-INFHLROL-Bd6W2W84.js} +1 -1
- package/dist/client/assets/sizeCapture-INFHLROL-Bd6W2W84.js.br +0 -0
- package/dist/client/assets/sizeCapture-INFHLROL-Bd6W2W84.js.gz +0 -0
- package/dist/client/assets/{sparkles-BKHm6SJH.js → sparkles-CnZrFoGY.js} +1 -1
- package/dist/client/assets/{stateDiagram-D77RDMKH-C7ooQSEd.js → stateDiagram-D77RDMKH-Dbp0hGDY.js} +1 -1
- package/dist/client/assets/stateDiagram-D77RDMKH-Dbp0hGDY.js.br +0 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-Dbp0hGDY.js.gz +0 -0
- package/dist/client/assets/stateDiagram-v2-MP3YSRHH-DC2wSMDB.js +1 -0
- package/dist/client/assets/{swimlanes-42K2YHIH-BvYBqq7o.js → swimlanes-42K2YHIH-C2Dr-53Y.js} +1 -1
- package/dist/client/assets/swimlanes-42K2YHIH-C2Dr-53Y.js.br +0 -0
- package/dist/client/assets/swimlanes-42K2YHIH-C2Dr-53Y.js.gz +0 -0
- package/dist/client/assets/swimlanesDiagram-VR7AAH4N-HYd7_s0l.js +8 -0
- package/dist/client/assets/{timeline-definition-24CTP7MA-Chgyw7xv.js → timeline-definition-24CTP7MA-D-bKj_yT.js} +1 -1
- package/dist/client/assets/timeline-definition-24CTP7MA-D-bKj_yT.js.br +0 -0
- package/dist/client/assets/timeline-definition-24CTP7MA-D-bKj_yT.js.gz +0 -0
- package/dist/client/assets/{triangle-alert-CcBfd16t.js → triangle-alert-D1EDVJ4z.js} +1 -1
- package/dist/client/assets/{vennDiagram-4TSXK5OY-CF1lMDLm.js → vennDiagram-4TSXK5OY-Dxo8DU3z.js} +1 -1
- package/dist/client/assets/vennDiagram-4TSXK5OY-Dxo8DU3z.js.br +0 -0
- package/dist/client/assets/vennDiagram-4TSXK5OY-Dxo8DU3z.js.gz +0 -0
- package/dist/client/assets/{wardleyDiagram-VM6X3IG4-CxUB2Gwu.js → wardleyDiagram-VM6X3IG4-BZMkLNpR.js} +1 -1
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-BZMkLNpR.js.br +0 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-BZMkLNpR.js.gz +0 -0
- package/dist/client/assets/{xychartDiagram-S5SC5T6Z-D1FK5tqF.js → xychartDiagram-S5SC5T6Z-CbxLQ8Sa.js} +1 -1
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-CbxLQ8Sa.js.br +0 -0
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-CbxLQ8Sa.js.gz +0 -0
- package/dist/client/index.html +1 -1
- package/dist/client/index.html.br +0 -0
- package/dist/client/index.html.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 +113 -33
- package/dist/server/agent/collaborationPeerTransport.js +441 -0
- package/dist/server/agent/collaborationPrompt.js +10 -4
- package/dist/server/agent/collaborationRoutes.js +75 -15
- package/dist/server/agent/collaborationStore.js +108 -23
- package/dist/server/agent/collaborationText.js +5 -0
- package/dist/server/agent/collaborationTmuxDelivery.js +9 -6
- package/dist/server/entry.js +42 -2
- package/dist/server/federation/runtime.js +7 -2
- package/dist/server/routes/terminal.js +48 -20
- package/package.json +1 -1
- package/runtime-manifest.json +3 -3
- package/dist/client/assets/App-Bl-MnHlK.js.br +0 -0
- package/dist/client/assets/App-Bl-MnHlK.js.gz +0 -0
- package/dist/client/assets/ChangeWalkthroughPanel-oVCcuJL3.js.br +0 -0
- package/dist/client/assets/ChangeWalkthroughPanel-oVCcuJL3.js.gz +0 -0
- package/dist/client/assets/DagPlayground-B33ZJSNC.js.br +0 -0
- package/dist/client/assets/DagPlayground-B33ZJSNC.js.gz +0 -0
- package/dist/client/assets/DiffLab-CZRP_SqH.js.br +0 -0
- package/dist/client/assets/DiffLab-CZRP_SqH.js.gz +0 -0
- package/dist/client/assets/DiffReview-BheUk7E5.js.br +0 -0
- package/dist/client/assets/DiffReview-BheUk7E5.js.gz +0 -0
- package/dist/client/assets/DiffReviewLab-DupHSbbd.js.br +0 -0
- package/dist/client/assets/DiffReviewLab-DupHSbbd.js.gz +0 -0
- package/dist/client/assets/DiffViewer-Cd5EE8E9.js.br +0 -0
- package/dist/client/assets/DiffViewer-Cd5EE8E9.js.gz +0 -0
- package/dist/client/assets/ModelPreview-Fnuxnt8A.js.br +0 -0
- package/dist/client/assets/ModelPreview-Fnuxnt8A.js.gz +0 -0
- package/dist/client/assets/RightSidebar-DTTmdNhf.js.br +0 -0
- package/dist/client/assets/RightSidebar-DTTmdNhf.js.gz +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-1oyW4gyC.js.br +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-1oyW4gyC.js.gz +0 -0
- package/dist/client/assets/app-CGZYqW3s.js +0 -35
- package/dist/client/assets/app-CGZYqW3s.js.br +0 -0
- package/dist/client/assets/app-CGZYqW3s.js.gz +0 -0
- package/dist/client/assets/arc-e9P469h4.js.br +0 -0
- package/dist/client/assets/arc-e9P469h4.js.gz +0 -0
- package/dist/client/assets/architectureDiagram-5GKGNRK7-CloNCKLm.js.br +0 -0
- package/dist/client/assets/architectureDiagram-5GKGNRK7-CloNCKLm.js.gz +0 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-D3wH4hOJ.js.br +0 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-D3wH4hOJ.js.gz +0 -0
- package/dist/client/assets/c4Diagram-7LVT6UL2-CX5tSHre.js.br +0 -0
- package/dist/client/assets/c4Diagram-7LVT6UL2-CX5tSHre.js.gz +0 -0
- package/dist/client/assets/channel-4mkr49cg.js +0 -1
- package/dist/client/assets/chunk-F27PBJKO-BRYx-Dtz.js.br +0 -0
- package/dist/client/assets/chunk-F27PBJKO-BRYx-Dtz.js.gz +0 -0
- package/dist/client/assets/chunk-IMKFNOWR-z6lXRY7P.js.br +0 -0
- package/dist/client/assets/chunk-IMKFNOWR-z6lXRY7P.js.gz +0 -0
- package/dist/client/assets/chunk-SVP7TREG-Bk-X0NaV.js.br +0 -0
- package/dist/client/assets/chunk-SVP7TREG-Bk-X0NaV.js.gz +0 -0
- package/dist/client/assets/chunk-TICWLB2K-BjUqsphB.js.br +0 -0
- package/dist/client/assets/chunk-TICWLB2K-BjUqsphB.js.gz +0 -0
- package/dist/client/assets/classDiagram-ZZMXUADV-DxYPnGqp.js +0 -1
- package/dist/client/assets/classDiagram-v2-VYDZK3BY-DxYPnGqp.js +0 -1
- package/dist/client/assets/cose-bilkent-JH36ORCC-Bw8F9aUq.js.br +0 -0
- package/dist/client/assets/cose-bilkent-JH36ORCC-Bw8F9aUq.js.gz +0 -0
- package/dist/client/assets/cynefin-OW5HDTMX-C5oB4ZTA.js.br +0 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-BkbwVdAD.js.br +0 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-BkbwVdAD.js.gz +0 -0
- package/dist/client/assets/dagre-GXQ25YYZ-8Jt1so7H.js.br +0 -0
- package/dist/client/assets/dagre-GXQ25YYZ-8Jt1so7H.js.gz +0 -0
- package/dist/client/assets/diagram-S7CK7UJ4-DyWUtoUO.js.br +0 -0
- package/dist/client/assets/diagram-S7CK7UJ4-DyWUtoUO.js.gz +0 -0
- package/dist/client/assets/diagram-UQ7AKVKN-BBnqsz2l.js.br +0 -0
- package/dist/client/assets/diagram-UQ7AKVKN-BBnqsz2l.js.gz +0 -0
- package/dist/client/assets/diagram-VSXAHHWV-DQ2JxOjW.js.br +0 -0
- package/dist/client/assets/diagram-VSXAHHWV-DQ2JxOjW.js.gz +0 -0
- package/dist/client/assets/diagram-VX7I27RA-Das3jLN9.js.br +0 -0
- package/dist/client/assets/diagram-VX7I27RA-Das3jLN9.js.gz +0 -0
- package/dist/client/assets/diagram-Z3DM3KII-0gbBMfrq.js.br +0 -0
- package/dist/client/assets/diagram-Z3DM3KII-0gbBMfrq.js.gz +0 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-DpAG2pFv.js.br +0 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-DpAG2pFv.js.gz +0 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-x4B0lUNv.js.br +0 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-x4B0lUNv.js.gz +0 -0
- package/dist/client/assets/flowDiagram-HODETNUW-Dx34Jzbo.js.gz +0 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-C7fWuwM6.js.br +0 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-C7fWuwM6.js.gz +0 -0
- package/dist/client/assets/gestureArbiter-CwYdj3ki.js.br +0 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-B6QlpyGY.js.br +0 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-B6QlpyGY.js.gz +0 -0
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-Bji-6SyT.js.br +0 -0
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-Bji-6SyT.js.gz +0 -0
- package/dist/client/assets/journeyDiagram-3NMN7TZE-FTfWDJ8K.js.br +0 -0
- package/dist/client/assets/journeyDiagram-3NMN7TZE-FTfWDJ8K.js.gz +0 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-CvrvLpNJ.js.br +0 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-CvrvLpNJ.js.gz +0 -0
- package/dist/client/assets/layout-D04TPU52.js.br +0 -0
- package/dist/client/assets/layout-D04TPU52.js.gz +0 -0
- package/dist/client/assets/linear-tmc9NdkO.js.br +0 -0
- package/dist/client/assets/linear-tmc9NdkO.js.gz +0 -0
- package/dist/client/assets/mermaid.core-BghOeJM9.js.br +0 -0
- package/dist/client/assets/mermaid.core-BghOeJM9.js.gz +0 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-BEhTGgsF.js.br +0 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-BEhTGgsF.js.gz +0 -0
- package/dist/client/assets/passwordBootstrap-wvzKljMS.js.br +0 -0
- package/dist/client/assets/passwordBootstrap-wvzKljMS.js.gz +0 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-BX1V7YDa.js.br +0 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-BX1V7YDa.js.gz +0 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-D5LMv0Qe.js.br +0 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-D5LMv0Qe.js.gz +0 -0
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-4fzegqtV.js.br +0 -0
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-4fzegqtV.js.gz +0 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-bgwdyvOY.js.br +0 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-bgwdyvOY.js.gz +0 -0
- package/dist/client/assets/referenceLongPress-Uw7_VECn.js.br +0 -0
- package/dist/client/assets/referenceLongPress-Uw7_VECn.js.gz +0 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-CZhlA-YX.js.br +0 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-CZhlA-YX.js.gz +0 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-BSLsSYgS.js.br +0 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-BSLsSYgS.js.gz +0 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-D5q4HIce.js.br +0 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-D5q4HIce.js.gz +0 -0
- package/dist/client/assets/sizeCapture-INFHLROL-C4i6fklz.js.br +0 -0
- package/dist/client/assets/sizeCapture-INFHLROL-C4i6fklz.js.gz +0 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-C7ooQSEd.js.br +0 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-C7ooQSEd.js.gz +0 -0
- package/dist/client/assets/stateDiagram-v2-MP3YSRHH-CYPjXGda.js +0 -1
- package/dist/client/assets/swimlanes-42K2YHIH-BvYBqq7o.js.br +0 -0
- package/dist/client/assets/swimlanes-42K2YHIH-BvYBqq7o.js.gz +0 -0
- package/dist/client/assets/swimlanesDiagram-VR7AAH4N-DGury37v.js +0 -8
- package/dist/client/assets/timeline-definition-24CTP7MA-Chgyw7xv.js.br +0 -0
- package/dist/client/assets/timeline-definition-24CTP7MA-Chgyw7xv.js.gz +0 -0
- package/dist/client/assets/vennDiagram-4TSXK5OY-CF1lMDLm.js.br +0 -0
- package/dist/client/assets/vennDiagram-4TSXK5OY-CF1lMDLm.js.gz +0 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-CxUB2Gwu.js.br +0 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-CxUB2Gwu.js.gz +0 -0
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-D1FK5tqF.js.br +0 -0
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-D1FK5tqF.js.gz +0 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { encryptedRequestSubject } from '../federation/requestContext.js';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { readFileSync, writeFileSync, renameSync, mkdirSync } from 'node:fs';
|
|
4
|
+
import { dirname } from 'node:path';
|
|
5
|
+
import WebSocket from 'ws';
|
|
6
|
+
import { AsyncQueue, toBase64, fromBase64, PacketChannel } from '../federation/packets.js';
|
|
7
|
+
import { secureConnection } from '../federation/secureProtocol.js';
|
|
8
|
+
import { socketDuplex } from '../federation/socketDuplex.js';
|
|
9
|
+
import { readPinnedCertificateAuthority } from '../federation/trustedCa.js';
|
|
10
|
+
import { COLLAB_LIMITS, STATUS_RANK } from './collaborationProtocol.js';
|
|
11
|
+
import { terminalMessage } from './collaborationStore.js';
|
|
12
|
+
/** The outer API gate already authenticated local CLI tokens. Browser callers
|
|
13
|
+
* additionally need durable administration authority; open-access sessions may
|
|
14
|
+
* not turn temporary access into persistent peer authorization. */
|
|
15
|
+
export function assertPeerRegistrationAuthority(req) {
|
|
16
|
+
const subjectId = encryptedRequestSubject(req);
|
|
17
|
+
if (!subjectId)
|
|
18
|
+
return;
|
|
19
|
+
const runtime = req.app.locals.passwordRuntime;
|
|
20
|
+
if (!runtime?.store.authorize({ subjectId, serviceId: runtime.serviceId, action: 'authorization.manage' }).allowed)
|
|
21
|
+
throw new Error('AUTHORIZATION_DENIED');
|
|
22
|
+
}
|
|
23
|
+
export function remoteSession(origin, id) { return `remote:${encodeURIComponent(origin)}:${encodeURIComponent(id)}`; }
|
|
24
|
+
function localSession(origin, id) {
|
|
25
|
+
const prefix = `remote:${encodeURIComponent(origin)}:`;
|
|
26
|
+
return id.startsWith(prefix) ? decodeURIComponent(id.slice(prefix.length)) : id;
|
|
27
|
+
}
|
|
28
|
+
export function validateCollaborationNode(input) {
|
|
29
|
+
const url = new URL(input.origin);
|
|
30
|
+
if (url.protocol !== 'https:' || url.origin !== input.origin || url.username || url.password
|
|
31
|
+
|| !/^12D3KooW[a-zA-Z0-9]{30,60}$/.test(input.serviceId)
|
|
32
|
+
|| (input.caFingerprint256 !== undefined && !/^(?:[\da-f]{2}:){31}[\da-f]{2}$/i.test(input.caFingerprint256)))
|
|
33
|
+
throw new Error('INVALID_COLLABORATION_NODE');
|
|
34
|
+
}
|
|
35
|
+
function mapContext(context, map) {
|
|
36
|
+
return { instructions: context.instructions ? { ...context.instructions, updatedBy: map(context.instructions.updatedBy) } : undefined,
|
|
37
|
+
roles: context.roles ? Object.fromEntries(Object.entries(context.roles).map(([id, role]) => [map(id), role])) : undefined,
|
|
38
|
+
roleVersions: context.roleVersions ? Object.fromEntries(Object.entries(context.roleVersions).map(([id, revision]) => [map(id), revision])) : undefined };
|
|
39
|
+
}
|
|
40
|
+
/** Small RPC client over the same authenticated Noise transport as the page.
|
|
41
|
+
* No cookies, browser device keys, or direct HTTP business requests. */
|
|
42
|
+
export class CollaborationRpc {
|
|
43
|
+
channel;
|
|
44
|
+
pending = new Map();
|
|
45
|
+
closed = false;
|
|
46
|
+
constructor(channel) {
|
|
47
|
+
this.channel = channel;
|
|
48
|
+
void this.read();
|
|
49
|
+
void channel.done.catch(error => this.close(error));
|
|
50
|
+
}
|
|
51
|
+
async read() {
|
|
52
|
+
try {
|
|
53
|
+
for await (const packet of this.channel.read()) {
|
|
54
|
+
const request = this.pending.get(packet.id);
|
|
55
|
+
if (!request)
|
|
56
|
+
continue;
|
|
57
|
+
clearTimeout(request.timer);
|
|
58
|
+
this.pending.delete(packet.id);
|
|
59
|
+
if (packet.type === 'error')
|
|
60
|
+
request.reject(new Error(String(packet.error)));
|
|
61
|
+
else
|
|
62
|
+
request.resolve(packet);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
this.close(error instanceof Error ? error : new Error('PEER_DISCONNECTED'));
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
this.close();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
request(packet) {
|
|
73
|
+
if (this.closed)
|
|
74
|
+
return Promise.reject(new Error('PEER_DISCONNECTED'));
|
|
75
|
+
if (this.pending.size >= 8)
|
|
76
|
+
return Promise.reject(new Error('PEER_BUSY'));
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
const id = randomUUID();
|
|
79
|
+
const timer = setTimeout(() => { this.pending.delete(id); reject(new Error('PEER_TIMEOUT')); this.close(); }, 10_000);
|
|
80
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
81
|
+
try {
|
|
82
|
+
this.channel.send({ ...packet, id });
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
this.close(error instanceof Error ? error : new Error('PEER_DISCONNECTED'));
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
close(error = new Error('PEER_DISCONNECTED')) {
|
|
90
|
+
if (this.closed)
|
|
91
|
+
return;
|
|
92
|
+
this.closed = true;
|
|
93
|
+
this.channel.close();
|
|
94
|
+
for (const item of this.pending.values()) {
|
|
95
|
+
clearTimeout(item.timer);
|
|
96
|
+
item.reject(error);
|
|
97
|
+
}
|
|
98
|
+
this.pending.clear();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export async function connectCollaborationRpc(identity, peer, endpoint) {
|
|
102
|
+
validateCollaborationNode(peer);
|
|
103
|
+
const ca = peer.caFingerprint256 ? await readPinnedCertificateAuthority(peer.origin, peer.caFingerprint256) : undefined;
|
|
104
|
+
const url = endpoint ?? `${peer.origin.replace(/^https:/, 'wss:')}/api/federation/secure`;
|
|
105
|
+
const socket = new WebSocket(url, { ca, rejectUnauthorized: true, followRedirects: false, handshakeTimeout: 3000, maxPayload: 1024 * 1024 });
|
|
106
|
+
const relay = endpoint ? relayDuplex(socket, peer.serviceId) : undefined;
|
|
107
|
+
const duplex = relay?.duplex ?? socketDuplex(socket);
|
|
108
|
+
try {
|
|
109
|
+
await new Promise((resolve, reject) => {
|
|
110
|
+
socket.once('open', resolve);
|
|
111
|
+
socket.once('error', reject);
|
|
112
|
+
socket.once('close', () => reject(new Error('PEER_DISCONNECTED')));
|
|
113
|
+
});
|
|
114
|
+
if (relay)
|
|
115
|
+
await relay.ready;
|
|
116
|
+
const secured = await secureConnection({ identity, duplex, initiator: true, targetPinnedPeerId: peer.serviceId, signal: AbortSignal.timeout(5000) });
|
|
117
|
+
return new CollaborationRpc(new PacketChannel(secured.duplex));
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
socket.terminate();
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function relayDuplex(socket, target) {
|
|
125
|
+
const queue = new AsyncQueue(64), streamId = randomUUID();
|
|
126
|
+
let readyResolve, readyReject;
|
|
127
|
+
const ready = new Promise((resolve, reject) => { readyResolve = resolve; readyReject = reject; });
|
|
128
|
+
void ready.catch(() => { });
|
|
129
|
+
const timer = setTimeout(() => fail(new Error('ROUTE_TIMEOUT')), 5000);
|
|
130
|
+
const fail = (error) => { clearTimeout(timer); readyReject(error); queue.end(error); socket.close(); };
|
|
131
|
+
const send = (frame) => {
|
|
132
|
+
if (socket.readyState !== 1 || socket.bufferedAmount > 1024 * 1024)
|
|
133
|
+
throw new Error('ROUTE_CONGESTED');
|
|
134
|
+
socket.send(JSON.stringify(frame));
|
|
135
|
+
};
|
|
136
|
+
socket.on('error', fail);
|
|
137
|
+
socket.on('close', () => fail(new Error('ROUTE_CLOSED')));
|
|
138
|
+
socket.on('message', (raw, binary) => {
|
|
139
|
+
try {
|
|
140
|
+
if (binary || raw.toString().length > 256 * 1024)
|
|
141
|
+
throw new Error('INVALID_RELAY_FRAME');
|
|
142
|
+
const frame = JSON.parse(raw.toString());
|
|
143
|
+
if (frame.type === 'ready') {
|
|
144
|
+
send({ type: 'open', streamId, serviceId: target });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (frame.streamId !== streamId)
|
|
148
|
+
return;
|
|
149
|
+
if (frame.type === 'opened') {
|
|
150
|
+
clearTimeout(timer);
|
|
151
|
+
readyResolve();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (frame.type !== 'data' || frame.encoding !== 'base64' || typeof frame.payload !== 'string')
|
|
155
|
+
throw new Error('ROUTE_CLOSED');
|
|
156
|
+
queue.push(fromBase64(frame.payload));
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
fail(error instanceof Error ? error : new Error('INVALID_RELAY_FRAME'));
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
return { ready, duplex: { source: queue, close: () => socket.close(), async sink(source) {
|
|
163
|
+
await ready;
|
|
164
|
+
for await (const bytes of source)
|
|
165
|
+
for (let offset = 0; offset < bytes.length; offset += 65536) {
|
|
166
|
+
while (socket.bufferedAmount > 256 * 1024) {
|
|
167
|
+
if (socket.readyState !== 1)
|
|
168
|
+
throw new Error('ROUTE_CLOSED');
|
|
169
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
170
|
+
}
|
|
171
|
+
send({ type: 'data', streamId, encoding: 'base64', payload: toBase64(bytes.subarray(offset, offset + 65536)) });
|
|
172
|
+
}
|
|
173
|
+
} } };
|
|
174
|
+
}
|
|
175
|
+
/** Group-scoped, durable server-to-server delivery. A binding is installed only
|
|
176
|
+
* by an already authorized group administrator, never by the remote peer itself. */
|
|
177
|
+
export class CollaborationPeerTransport {
|
|
178
|
+
options;
|
|
179
|
+
bindings = [];
|
|
180
|
+
clients = new Map();
|
|
181
|
+
running = new Set();
|
|
182
|
+
accepted = new Set();
|
|
183
|
+
shellConfirmed = new Set();
|
|
184
|
+
nextAttempt = new Map();
|
|
185
|
+
timer;
|
|
186
|
+
stopped = false;
|
|
187
|
+
constructor(options) {
|
|
188
|
+
this.options = options;
|
|
189
|
+
try {
|
|
190
|
+
const data = JSON.parse(readFileSync(options.file, 'utf8'));
|
|
191
|
+
if (data.version === 1 && Array.isArray(data.bindings))
|
|
192
|
+
this.bindings = data.bindings.filter((b) => {
|
|
193
|
+
try {
|
|
194
|
+
validateCollaborationNode(b.peer);
|
|
195
|
+
return typeof b.groupId === 'string' && new URL(b.localOrigin).origin === b.localOrigin;
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
catch { /* First run has no server authorizations. */ }
|
|
203
|
+
}
|
|
204
|
+
notify() {
|
|
205
|
+
for (const b of this.bindings) {
|
|
206
|
+
const key = `${b.groupId}:${b.peer.serviceId}`;
|
|
207
|
+
if (!this.options.store.federationSnapshot().messages.some(m => m.groupId === b.groupId && m.status === 'pending'
|
|
208
|
+
&& this.options.store.diagnostic(m.id)?.last_error))
|
|
209
|
+
this.nextAttempt.delete(key);
|
|
210
|
+
}
|
|
211
|
+
// A queued message may arrive while an empty scan is finishing.
|
|
212
|
+
setImmediate(() => this.wake());
|
|
213
|
+
}
|
|
214
|
+
start() { this.options.store.onMessageQueued = () => this.notify(); this.timer = setInterval(() => this.wake(), 500); this.timer.unref(); this.wake(); }
|
|
215
|
+
close() { this.stopped = true; this.options.store.onMessageQueued = undefined; clearInterval(this.timer); for (const client of this.clients.values())
|
|
216
|
+
void client.then(c => c.close()).catch(() => { }); this.clients.clear(); }
|
|
217
|
+
live(b) {
|
|
218
|
+
const group = this.options.store.getGroup(b.groupId);
|
|
219
|
+
return !!group && !group.deleted && group.federated && group.sessionIds.some(id => id.startsWith(`remote:${encodeURIComponent(b.peer.origin)}:`));
|
|
220
|
+
}
|
|
221
|
+
configure(groupId, localOrigin, nodes) {
|
|
222
|
+
if (!Array.isArray(nodes) || nodes.length > 64)
|
|
223
|
+
throw new Error('INVALID_COLLABORATION_NODES');
|
|
224
|
+
const group = this.options.store.getGroup(groupId);
|
|
225
|
+
if (!group || group.deleted || !group.federated)
|
|
226
|
+
throw new Error('GROUP_NOT_FOUND');
|
|
227
|
+
const local = nodes.find(n => n.serviceId === this.options.serviceId);
|
|
228
|
+
if (!local || local.origin !== localOrigin)
|
|
229
|
+
throw new Error('LOCAL_NODE_MISMATCH');
|
|
230
|
+
for (const node of nodes)
|
|
231
|
+
validateCollaborationNode(node);
|
|
232
|
+
const peers = nodes.filter(n => n.serviceId !== this.options.serviceId && group.sessionIds.some(id => id.startsWith(`remote:${encodeURIComponent(n.origin)}:`)));
|
|
233
|
+
const next = [...this.bindings.filter(b => b.groupId !== groupId), ...peers.map(peer => ({ groupId, localOrigin, peer }))];
|
|
234
|
+
if (JSON.stringify(next) !== JSON.stringify(this.bindings)) {
|
|
235
|
+
mkdirSync(dirname(this.options.file), { recursive: true, mode: 0o700 });
|
|
236
|
+
const temp = `${this.options.file}.${process.pid}.tmp`;
|
|
237
|
+
writeFileSync(temp, JSON.stringify({ version: 1, bindings: next }), { mode: 0o600 });
|
|
238
|
+
renameSync(temp, this.options.file);
|
|
239
|
+
this.bindings = next;
|
|
240
|
+
this.nextAttempt.clear();
|
|
241
|
+
for (const client of this.clients.values())
|
|
242
|
+
void client.then(c => c.close()).catch(() => { });
|
|
243
|
+
this.clients.clear();
|
|
244
|
+
}
|
|
245
|
+
this.notify();
|
|
246
|
+
}
|
|
247
|
+
canRoute(subjectId, targetId) {
|
|
248
|
+
return this.bindings.some(b => b.peer.serviceId === subjectId && this.live(b)
|
|
249
|
+
&& this.bindings.some(target => target.groupId === b.groupId && target.peer.serviceId === targetId && this.live(target)));
|
|
250
|
+
}
|
|
251
|
+
binding(subjectId, groupId) {
|
|
252
|
+
const binding = this.bindings.find(b => b.peer.serviceId === subjectId && b.groupId === groupId && this.live(b));
|
|
253
|
+
if (!binding)
|
|
254
|
+
throw new Error('COLLABORATION_NOT_AUTHORIZED');
|
|
255
|
+
return binding;
|
|
256
|
+
}
|
|
257
|
+
/** Remote peers can submit only their own members' messages and query receipts
|
|
258
|
+
* for those messages. They cannot create groups, rewrite bodies or administer sessions. */
|
|
259
|
+
receive(subjectId, packet) {
|
|
260
|
+
const b = this.binding(subjectId, String(packet.groupId));
|
|
261
|
+
const store = this.options.store;
|
|
262
|
+
if (packet.context)
|
|
263
|
+
store.mergeContext(b.groupId, mapContext(packet.context, id => localSession(b.localOrigin, id)));
|
|
264
|
+
if (packet.message) {
|
|
265
|
+
const message = packet.message;
|
|
266
|
+
if (message.groupId !== b.groupId || typeof message.fromSessionId !== 'string' || message.fromSessionId.startsWith('remote:')
|
|
267
|
+
|| typeof message.toSessionId !== 'string' || !message.toSessionId.startsWith(`remote:${encodeURIComponent(b.localOrigin)}:`)
|
|
268
|
+
|| Buffer.byteLength(JSON.stringify(message)) > COLLAB_LIMITS.wire_bytes)
|
|
269
|
+
throw new Error('INVALID_PEER_MESSAGE');
|
|
270
|
+
const incoming = { ...message, fromSessionId: remoteSession(b.peer.origin, message.fromSessionId),
|
|
271
|
+
toSessionId: localSession(b.localOrigin, message.toSessionId), status: 'pending', deliveredAt: null, readAt: null,
|
|
272
|
+
snapshot: null, deliverySource: undefined, readSource: undefined, shellConfirmed: message.shellConfirmed === true };
|
|
273
|
+
const existing = store.getMessage(message.id);
|
|
274
|
+
if (existing && (existing.groupId !== incoming.groupId || existing.fromSessionId !== incoming.fromSessionId
|
|
275
|
+
|| existing.toSessionId !== incoming.toSessionId || existing.content !== incoming.content))
|
|
276
|
+
throw new Error('MESSAGE_ID_CONFLICT');
|
|
277
|
+
if (!existing)
|
|
278
|
+
store.mergeFederatedMessages([incoming]);
|
|
279
|
+
else if (incoming.shellConfirmed && !existing.shellConfirmed)
|
|
280
|
+
store.mergeFederatedMessages([{ ...existing, shellConfirmed: true }]);
|
|
281
|
+
if (!store.getMessage(message.id))
|
|
282
|
+
throw new Error('PEER_REJECTED_MESSAGE');
|
|
283
|
+
this.options.deliver(incoming.toSessionId);
|
|
284
|
+
}
|
|
285
|
+
if (!Array.isArray(packet.ids) || packet.ids.length > 20 || packet.ids.some(id => typeof id !== 'string'))
|
|
286
|
+
throw new Error('INVALID_RECEIPT_IDS');
|
|
287
|
+
const receipts = packet.ids.flatMap(id => {
|
|
288
|
+
const message = store.getMessage(id);
|
|
289
|
+
if (!message || message.groupId !== b.groupId || !message.fromSessionId?.startsWith(`remote:${encodeURIComponent(b.peer.origin)}:`)
|
|
290
|
+
|| message.toSessionId.startsWith('remote:'))
|
|
291
|
+
return [];
|
|
292
|
+
const facts = terminalMessage(message);
|
|
293
|
+
const known = Array.isArray(packet.known) ? packet.known.find((item) => item?.id === message.id) : undefined;
|
|
294
|
+
if (known && known.status === facts.status && known.deliveredAt === facts.deliveredAt && known.shellConfirmed === message.shellConfirmed && (known.hasSnapshot || !message.snapshot))
|
|
295
|
+
return [];
|
|
296
|
+
const receipt = store.receipt(message.id);
|
|
297
|
+
return [{ id: message.id, status: facts.status, deliveredAt: facts.deliveredAt,
|
|
298
|
+
snapshot: (receipt.snapshot ?? '').slice(0, 16000) || null, deliverySource: facts.deliverySource,
|
|
299
|
+
failureReason: message.failureReason, shellConfirmed: message.shellConfirmed, last_error: store.diagnostic(message.id)?.last_error ?? null }];
|
|
300
|
+
});
|
|
301
|
+
const group = store.getGroup(b.groupId);
|
|
302
|
+
const activity = this.options.activity?.().filter(item => group.sessionIds.includes(item.sessionId) && !item.sessionId.startsWith('remote:')) ?? [];
|
|
303
|
+
return { activity, context: mapContext(store.context(b.groupId), id => id.startsWith('remote:') ? id : remoteSession(b.localOrigin, id)), receipts, receivedIds: packet.ids.filter(id => {
|
|
304
|
+
const message = store.getMessage(id);
|
|
305
|
+
return message?.groupId === b.groupId && message.fromSessionId?.startsWith(`remote:${encodeURIComponent(b.peer.origin)}:`) && !message.toSessionId.startsWith('remote:');
|
|
306
|
+
}) };
|
|
307
|
+
}
|
|
308
|
+
wake() {
|
|
309
|
+
if (this.stopped)
|
|
310
|
+
return;
|
|
311
|
+
for (const b of this.bindings) {
|
|
312
|
+
const key = `${b.groupId}:${b.peer.serviceId}`;
|
|
313
|
+
if (!this.live(b) || this.running.has(key) || (this.nextAttempt.get(key) ?? 0) > Date.now())
|
|
314
|
+
continue;
|
|
315
|
+
this.running.add(key);
|
|
316
|
+
void this.sync(b).catch(() => { this.nextAttempt.set(key, Date.now() + 1000); }).finally(() => this.running.delete(key));
|
|
317
|
+
}
|
|
318
|
+
// No registration must be visible, rather than an unexplained zero-attempt queue.
|
|
319
|
+
for (const group of this.options.store.list())
|
|
320
|
+
for (const m of this.options.store.federationSnapshot().messages.filter(message => message.groupId === group.id)) {
|
|
321
|
+
if (m.status !== 'pending' || m.fromSessionId?.startsWith('remote:') || !m.toSessionId.startsWith('remote:'))
|
|
322
|
+
continue;
|
|
323
|
+
if (this.bindings.some(b => b.groupId === group.id && this.live(b) && m.toSessionId.startsWith(`remote:${encodeURIComponent(b.peer.origin)}:`)))
|
|
324
|
+
continue;
|
|
325
|
+
const prior = this.options.store.diagnostic(m.id);
|
|
326
|
+
if (prior?.last_error === 'PEER_REGISTRATION_REQUIRED' && Date.now() - prior.checked_at < 10_000)
|
|
327
|
+
continue;
|
|
328
|
+
this.options.store.recordTransport(m.id, { relay_online: null, peer_reachable: null, attempt_count: prior?.attempt_count ?? 0,
|
|
329
|
+
next_retry_at: null, last_error: 'PEER_REGISTRATION_REQUIRED', checked_at: Date.now() });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
async client(b) {
|
|
333
|
+
const key = b.peer.serviceId;
|
|
334
|
+
let pending = this.clients.get(key);
|
|
335
|
+
if (pending) {
|
|
336
|
+
const client = await pending;
|
|
337
|
+
if (!client.closed)
|
|
338
|
+
return client;
|
|
339
|
+
this.clients.delete(key);
|
|
340
|
+
}
|
|
341
|
+
pending = (async () => {
|
|
342
|
+
try {
|
|
343
|
+
return await this.options.connect(b.peer);
|
|
344
|
+
}
|
|
345
|
+
catch (directError) {
|
|
346
|
+
for (const entry of this.bindings.filter(e => e.groupId === b.groupId && e.peer.serviceId !== b.peer.serviceId && this.live(e))) {
|
|
347
|
+
try {
|
|
348
|
+
return await this.options.connect(b.peer, entry.peer);
|
|
349
|
+
}
|
|
350
|
+
catch { /* Try the next explicitly registered entry. */ }
|
|
351
|
+
}
|
|
352
|
+
throw directError;
|
|
353
|
+
}
|
|
354
|
+
})();
|
|
355
|
+
this.clients.set(key, pending);
|
|
356
|
+
try {
|
|
357
|
+
return await pending;
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
if (this.clients.get(key) === pending)
|
|
361
|
+
this.clients.delete(key);
|
|
362
|
+
throw error;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
async sync(b) {
|
|
366
|
+
const store = this.options.store, key = `${b.groupId}:${b.peer.serviceId}`;
|
|
367
|
+
const messages = store.federationSnapshot().messages.filter(m => m.groupId === b.groupId).filter(m => !m.fromSessionId?.startsWith('remote:') && m.fromSessionId !== null
|
|
368
|
+
&& m.toSessionId.startsWith(`remote:${encodeURIComponent(b.peer.origin)}:`) && !['failed', 'expired'].includes(m.status)
|
|
369
|
+
&& (m.status === 'pending' || !m.snapshot)).sort((a, b) => Number(b.status === 'pending') - Number(a.status === 'pending'));
|
|
370
|
+
const pending = messages.filter(m => m.status === 'pending');
|
|
371
|
+
const targetErrors = new Map();
|
|
372
|
+
try {
|
|
373
|
+
const client = await this.client(b);
|
|
374
|
+
for (let offset = 0; offset < Math.max(1, messages.length); offset += 20) {
|
|
375
|
+
const batch = messages.slice(offset, offset + 20);
|
|
376
|
+
const requests = batch.filter(m => m.status === 'pending' && (!this.accepted.has(m.id) || m.shellConfirmed && !this.shellConfirmed.has(m.id)));
|
|
377
|
+
const payloads = requests.length ? requests : [undefined];
|
|
378
|
+
for (const message of payloads) {
|
|
379
|
+
const result = await client.request({ type: 'collaboration-exchange', groupId: b.groupId,
|
|
380
|
+
context: mapContext(store.context(b.groupId), id => id.startsWith('remote:') ? id : remoteSession(b.localOrigin, id)),
|
|
381
|
+
ids: batch.map(m => m.id), known: batch.map(m => ({ id: m.id, status: m.status, deliveredAt: m.deliveredAt, hasSnapshot: !!m.snapshot, shellConfirmed: m.shellConfirmed })), ...(message ? { message } : {}) });
|
|
382
|
+
if (Array.isArray(result.activity))
|
|
383
|
+
for (const item of result.activity.slice(0, 500)) {
|
|
384
|
+
if (!item || typeof item.sessionId !== 'string' || item.sessionId.startsWith('remote:'))
|
|
385
|
+
continue;
|
|
386
|
+
const id = remoteSession(b.peer.origin, item.sessionId);
|
|
387
|
+
if (store.getGroup(b.groupId)?.sessionIds.includes(id))
|
|
388
|
+
store.recordPeerActivity(id, item.last_terminal_output_at, item.activity_observed_at);
|
|
389
|
+
}
|
|
390
|
+
if (result.context)
|
|
391
|
+
store.mergeContext(b.groupId, mapContext(result.context, id => localSession(b.localOrigin, id)));
|
|
392
|
+
if (Array.isArray(result.receivedIds))
|
|
393
|
+
for (const item of batch) {
|
|
394
|
+
if (result.receivedIds.includes(item.id))
|
|
395
|
+
this.accepted.add(item.id);
|
|
396
|
+
else
|
|
397
|
+
this.accepted.delete(item.id);
|
|
398
|
+
}
|
|
399
|
+
if (this.accepted.size > 2000) {
|
|
400
|
+
const retained = new Set(store.federationSnapshot().messages.map(m => m.id));
|
|
401
|
+
for (const id of this.accepted)
|
|
402
|
+
if (!retained.has(id)) {
|
|
403
|
+
this.accepted.delete(id);
|
|
404
|
+
this.shellConfirmed.delete(id);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (!Array.isArray(result.receipts))
|
|
408
|
+
throw new Error('INVALID_PEER_RECEIPTS');
|
|
409
|
+
for (const receipt of result.receipts) {
|
|
410
|
+
const existing = batch.find(m => m.id === receipt.id);
|
|
411
|
+
if (!existing)
|
|
412
|
+
continue;
|
|
413
|
+
if (receipt.shellConfirmed)
|
|
414
|
+
this.shellConfirmed.add(existing.id);
|
|
415
|
+
if (typeof receipt.last_error === 'string')
|
|
416
|
+
targetErrors.set(existing.id, receipt.last_error);
|
|
417
|
+
if (!receipt.status || !Object.hasOwn(STATUS_RANK, receipt.status)
|
|
418
|
+
|| (receipt.deliveredAt != null && !Number.isFinite(receipt.deliveredAt)))
|
|
419
|
+
throw new Error('INVALID_PEER_RECEIPT');
|
|
420
|
+
store.mergeFederatedMessages([{ ...existing, status: receipt.status === 'read' ? 'delivered' : receipt.status, deliveredAt: receipt.deliveredAt ?? null,
|
|
421
|
+
readAt: null, snapshot: typeof receipt.snapshot === 'string' ? receipt.snapshot.slice(0, 16000) : null,
|
|
422
|
+
deliverySource: receipt.deliverySource, readSource: undefined, failureReason: receipt.failureReason }]);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
for (const m of pending)
|
|
427
|
+
store.recordTransport(m.id, { relay_online: true, peer_reachable: true,
|
|
428
|
+
attempt_count: (store.diagnostic(m.id)?.attempt_count ?? 0) + 1, next_retry_at: m.status === 'pending' ? Date.now() + 500 : null, last_error: targetErrors.get(m.id) ?? null, checked_at: Date.now(),
|
|
429
|
+
...(this.accepted.has(m.id) ? { remote_received_at: store.diagnostic(m.id)?.remote_received_at ?? Date.now() } : {}) });
|
|
430
|
+
this.nextAttempt.set(key, Date.now() + (messages.some(m => m.status === 'pending') ? 250 : 1000));
|
|
431
|
+
}
|
|
432
|
+
catch (error) {
|
|
433
|
+
const attempts = Math.max(0, ...pending.map(m => store.diagnostic(m.id)?.attempt_count ?? 0)) + 1;
|
|
434
|
+
const next = Date.now() + Math.min(10_000, 500 * 2 ** Math.min(attempts - 1, 5));
|
|
435
|
+
this.nextAttempt.set(key, next);
|
|
436
|
+
for (const m of pending)
|
|
437
|
+
store.recordTransport(m.id, { relay_online: true, peer_reachable: false, attempt_count: attempts, remote_received_at: store.diagnostic(m.id)?.remote_received_at,
|
|
438
|
+
next_retry_at: next, last_error: error instanceof Error ? error.message.slice(0, 300) : 'PEER_UNREACHABLE', checked_at: Date.now() });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
@@ -3,7 +3,7 @@ import { sanitizeCollaborationRole } from './collaborationStore.js';
|
|
|
3
3
|
const SHELL_RULE = '─'.repeat(30);
|
|
4
4
|
/** Body larger than this (in UTF-8 bytes) is replaced by a retrieval pointer
|
|
5
5
|
* instead of being injected into the terminal verbatim. */
|
|
6
|
-
const MAX_INLINE_BODY_BYTES =
|
|
6
|
+
const MAX_INLINE_BODY_BYTES = 4_096;
|
|
7
7
|
/**
|
|
8
8
|
* Characters that would corrupt the delivery shell header `「X」群(N 个成员)`
|
|
9
9
|
* and the per-message `来自:X · kind` line: the quote brackets and the middle
|
|
@@ -73,6 +73,12 @@ function formatCollaborationMessage(message, source, fannedNames, fence, token)
|
|
|
73
73
|
const lines = [`来自:${source} · ${message.kind}${fannedNames.length ? ' · 群发' : ''}`];
|
|
74
74
|
if (fannedNames.length)
|
|
75
75
|
lines.push(`同时发给了:${fannedNames.join('、')}`);
|
|
76
|
+
const rules = message.instructions;
|
|
77
|
+
if (rules?.text) {
|
|
78
|
+
lines.push(`群协作约定(版本 ${rules.version},不改变用户授权):`);
|
|
79
|
+
lines.push(Buffer.byteLength(rules.text) <= 1024 ? rules.text
|
|
80
|
+
: `执行 td collab message get ${token} --text 查看本消息附带的完整群规与正文。`);
|
|
81
|
+
}
|
|
76
82
|
lines.push('', fence, message.content, fence);
|
|
77
83
|
if (message.task)
|
|
78
84
|
lines.push('', `任务上报:${JSON.stringify(message.task)}`);
|
|
@@ -92,7 +98,7 @@ export function formatCollaborationDelivery(input) {
|
|
|
92
98
|
const token = tokens.get(message.id) ?? message.id;
|
|
93
99
|
const bytes = Buffer.byteLength(message.content);
|
|
94
100
|
const body = bytes > MAX_INLINE_BODY_BYTES
|
|
95
|
-
? `大消息已完整保存(${bytes}
|
|
101
|
+
? `大消息已完整保存(${bytes} 字节)。先执行 td collab message get ${token} --text 查看完整正文;不要把这条提示当作消息正文。`
|
|
96
102
|
: message.content;
|
|
97
103
|
const fence = '`'.repeat(Math.max(3, ...Array.from(body.matchAll(/`+/g), (match) => match[0].length + 1)));
|
|
98
104
|
const fannedNames = (message.fanOutIds ?? [])
|
|
@@ -122,13 +128,13 @@ export function formatCollaborationDelivery(input) {
|
|
|
122
128
|
const routingHelp = input.showRoutingHelp === false ? [] : (peers.length ? ['联系其他成员:', ...peers] : []);
|
|
123
129
|
const dynamicNotices = [
|
|
124
130
|
...(peerIds.some((id) => id.startsWith('remote:'))
|
|
125
|
-
? ['跨服务通信使用 td collab
|
|
131
|
+
? ['跨服务通信使用 td collab;已登记节点由服务后台直接投递,使用 message get 查看送达回执与重试原因。'] : []),
|
|
126
132
|
...unreachable.map((id) => {
|
|
127
133
|
const session = sessionsById.get(id);
|
|
128
134
|
return `注意:${session ? sanitizeCollaborationName(session.name) : id} 服务不可达,消息无法送达;仅可排队等待重连。`;
|
|
129
135
|
}),
|
|
130
136
|
];
|
|
131
|
-
const captureHelp = '想看伙伴正在做什么、是否卡住:先用 `td collab capture <会话ID> --text`
|
|
137
|
+
const captureHelp = '想看伙伴正在做什么、是否卡住:先用 `td collab capture <会话ID> --text` 只读当前屏幕;可加 --lines 200 查看历史,不打断对方;ID 用 `td collab status --text` 查。仅同组本机 tmux;远端用 send 询问。快照不等于任务完成,按需查看,避免循环轮询。';
|
|
132
138
|
const notes = [...routingHelp, ...dynamicNotices, '更多操作:`td collab --help`。',
|
|
133
139
|
...(input.showRoutingHelp === false ? [] : [captureHelp])];
|
|
134
140
|
// Notes (education + dynamic notices) live inside the shell, after the last
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { terminalMessage } from './collaborationStore.js';
|
|
2
|
+
import { assertPeerRegistrationAuthority } from './collaborationPeerTransport.js';
|
|
1
3
|
import { Router } from 'express';
|
|
2
4
|
import { COLLAB_LIMITS, CollaborationError, MIN_ID_PREFIX_LENGTH, ambiguousIdMessage, extrasFromBody } from './collaborationProtocol.js';
|
|
3
5
|
/** Member names arrive agent-authored over a CLI; control characters would
|
|
@@ -21,6 +23,27 @@ export function collaborationRoutes({ store, resolveSession, deliver, rebind, re
|
|
|
21
23
|
code: error instanceof CollaborationError ? error.code : 'COLLABORATION_ERROR', error: error instanceof Error ? error.message : String(error) });
|
|
22
24
|
}
|
|
23
25
|
};
|
|
26
|
+
router.get('/transport', run((req, res) => {
|
|
27
|
+
if (!req.app.locals.collaborationNode)
|
|
28
|
+
throw new Error('COLLABORATION_UNAVAILABLE');
|
|
29
|
+
res.json({ ok: true, node: req.app.locals.collaborationNode });
|
|
30
|
+
}));
|
|
31
|
+
router.post('/transport', run((req, res, sessionId) => {
|
|
32
|
+
assertPeerRegistrationAuthority(req);
|
|
33
|
+
const resolved = store.resolveGroupId(String(req.body.groupId ?? ''));
|
|
34
|
+
if (resolved.status !== 'ok')
|
|
35
|
+
throw new Error('GROUP_NOT_FOUND_OR_AMBIGUOUS');
|
|
36
|
+
if (!store.groupsForSession(sessionId).some(group => group.id === resolved.id))
|
|
37
|
+
throw new Error('GROUP_NOT_FOUND');
|
|
38
|
+
const node = req.app.locals.collaborationNode;
|
|
39
|
+
if (!node || !req.app.locals.collaborationTransport || !Array.isArray(req.body.nodes))
|
|
40
|
+
throw new Error('COLLABORATION_UNAVAILABLE');
|
|
41
|
+
const local = req.body.nodes.find((item) => item?.serviceId === node.serviceId);
|
|
42
|
+
if (!local)
|
|
43
|
+
throw new Error('LOCAL_NODE_MISMATCH');
|
|
44
|
+
req.app.locals.collaborationTransport.configure(resolved.id, local.origin, req.body.nodes);
|
|
45
|
+
res.json({ ok: true, group_id: resolved.id, transport: 'server' });
|
|
46
|
+
}));
|
|
24
47
|
router.post('/route/rebind', run(async (req, res, sessionId) => {
|
|
25
48
|
if (!rebind)
|
|
26
49
|
throw new CollaborationError('ROUTE_REBIND_UNAVAILABLE', 'Route rebinding is not available');
|
|
@@ -87,10 +110,10 @@ export function collaborationRoutes({ store, resolveSession, deliver, rebind, re
|
|
|
87
110
|
router.get('/capabilities', run((_req, res) => {
|
|
88
111
|
res.json({ protocol_version: 2, limits: COLLAB_LIMITS,
|
|
89
112
|
routing: { background_recovery: true, explicit_rebind: Boolean(rebind), fixed_tmux_pane: true },
|
|
90
|
-
statuses: ['pending', 'delivered', '
|
|
91
|
-
semantics: { delivered: 'written to terminal; no application acknowledgement implied',
|
|
113
|
+
statuses: ['pending', 'delivered', 'failed', 'expired'], queued_status: 'pending',
|
|
114
|
+
semantics: { delivered: 'written to terminal; no application acknowledgement implied',
|
|
92
115
|
ack: 'explicit response_kind=ack', result: 'explicit response_kind=result; inspect task.status and evidence',
|
|
93
|
-
|
|
116
|
+
snapshot: 'terminal observation, never proof of reading or task completion',
|
|
94
117
|
timeout: 'stops waiting, does not cancel delivery',
|
|
95
118
|
retry: 'same message id may be submitted again after a crash or uncertain transport result; consumers deduplicate by message id' } });
|
|
96
119
|
}));
|
|
@@ -141,14 +164,13 @@ export function collaborationRoutes({ store, resolveSession, deliver, rebind, re
|
|
|
141
164
|
const messages = store.send({ ...extrasFromBody(req.body), groupId: original.groupId, fromSessionId: sessionId,
|
|
142
165
|
toSessionIds: [original.fromSessionId], kind: 'reply', content: typeof req.body.content === 'string' ? req.body.content : '',
|
|
143
166
|
replyTo: original.id, threadId: original.threadId });
|
|
144
|
-
// Reply is explicit consumption; only mark after successful validation/persistence.
|
|
145
|
-
store.markRead([original.id]);
|
|
146
167
|
await awaitDelivery(messages[0].id, (await deliver(original.fromSessionId)) ?? null);
|
|
147
168
|
sent(res, messages.map((message) => message.id));
|
|
148
169
|
}));
|
|
149
170
|
router.get('/message/:id', run((req, res, sessionId) => {
|
|
150
171
|
const message = ownMessage(String(req.params.id), sessionId);
|
|
151
|
-
|
|
172
|
+
const raw = req.query.raw === 'true';
|
|
173
|
+
res.json({ ok: true, ...store.receipt(message.id, raw), ...(req.query.receipt_only === 'true' ? {} : { message: terminalMessage(message, raw) }) });
|
|
152
174
|
}));
|
|
153
175
|
router.post('/message/:id/confirm-shell', run(async (req, res, sessionId) => {
|
|
154
176
|
const message = ownMessage(String(req.params.id), sessionId);
|
|
@@ -159,10 +181,10 @@ export function collaborationRoutes({ store, resolveSession, deliver, rebind, re
|
|
|
159
181
|
await awaitDelivery(message.id, outcome);
|
|
160
182
|
sent(res, [message.id]);
|
|
161
183
|
}));
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
184
|
+
// Old clients receive an explicit error instead of a fabricated read receipt.
|
|
185
|
+
router.post('/message/:id/read', run((req, _res, sessionId) => {
|
|
186
|
+
ownMessage(String(req.params.id), sessionId, true);
|
|
187
|
+
throw new CollaborationError('READ_RECEIPTS_UNSUPPORTED', 'Terminal delivery cannot observe reading; use message get for the snapshot or reply for an explicit response', 410);
|
|
166
188
|
}));
|
|
167
189
|
router.get('/inbox', run((req, res, sessionId) => {
|
|
168
190
|
const string = (key) => typeof req.query[key] === 'string' ? req.query[key] : undefined;
|
|
@@ -185,14 +207,29 @@ export function collaborationRoutes({ store, resolveSession, deliver, rebind, re
|
|
|
185
207
|
if (resolved.status === 'ok')
|
|
186
208
|
threadFilter = resolved.id;
|
|
187
209
|
}
|
|
188
|
-
|
|
210
|
+
let fromFilter = string('from');
|
|
211
|
+
if (fromFilter) {
|
|
212
|
+
const allowedIds = [...new Set(store.groupsForSession(sessionId).flatMap(group => group.sessionIds))];
|
|
213
|
+
if (!allowedIds.includes(fromFilter)) {
|
|
214
|
+
const matches = fromFilter.length < 4 ? [] : allowedIds.filter(id => id.startsWith(fromFilter)
|
|
215
|
+
|| (id.startsWith('remote:') && decodeURIComponent(id.slice(id.lastIndexOf(':') + 1)).startsWith(fromFilter)));
|
|
216
|
+
if (matches.length > 1)
|
|
217
|
+
throw new CollaborationError('SESSION_ID_AMBIGUOUS', '发送者短 ID 匹配多个成员,请使用完整 ID');
|
|
218
|
+
if (matches.length === 1)
|
|
219
|
+
fromFilter = matches[0];
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (string('unread') === 'true')
|
|
223
|
+
throw new CollaborationError('READ_RECEIPTS_UNSUPPORTED', 'Use a consumer cursor to track retrieved messages');
|
|
224
|
+
const page = store.page(sessionId, { since, afterId: string('after_id'), cursor: string('cursor'),
|
|
189
225
|
consumer: string('consumer'), limit: string('limit') ? Number(string('limit')) : undefined,
|
|
190
|
-
from:
|
|
226
|
+
from: fromFilter, group: groupFilter, thread: threadFilter, kind: string('kind'), responseKind: string('response_kind'), order: string('order') });
|
|
227
|
+
const messages = page.messages.map(message => terminalMessage(message, string('raw') === 'true'));
|
|
191
228
|
// Fan-out edges carry sibling recipients; the names map lets the CLI text
|
|
192
229
|
// renderer show 同时发给了:… as people instead of bare session ids. The
|
|
193
230
|
// fallback to the raw id stays server-side agnostic (federated/offline).
|
|
194
231
|
const nameIds = [...new Set(page.messages.flatMap((message) => [message.fromSessionId, ...(message.fanOutIds ?? [])].filter((id) => typeof id === 'string' && id.length > 0)))];
|
|
195
|
-
res.json({ ok: true, ...page, ...(nameIds.length ? { names: resolveNames?.(nameIds) ?? {} } : {}) });
|
|
232
|
+
res.json({ ok: true, ...page, messages, ...(nameIds.length ? { names: resolveNames?.(nameIds) ?? {} } : {}) });
|
|
196
233
|
}));
|
|
197
234
|
router.post('/cursor/commit', run((req, res, sessionId) => {
|
|
198
235
|
store.commitCursor(sessionId, String(req.body.cursor ?? ''), String(req.body.consumer ?? ''));
|
|
@@ -212,6 +249,16 @@ export function collaborationRoutes({ store, resolveSession, deliver, rebind, re
|
|
|
212
249
|
// Member names ride along so role lists read as people, not bare ids; the
|
|
213
250
|
// caller falls back to the session id when a name is unknown (federated or
|
|
214
251
|
// offline members have no local record).
|
|
252
|
+
const memberOf = (ids, input) => {
|
|
253
|
+
if (ids.includes(input))
|
|
254
|
+
return input;
|
|
255
|
+
if (input.length < 4)
|
|
256
|
+
return null;
|
|
257
|
+
const matches = ids.filter(id => id.startsWith(input) || (id.startsWith('remote:') && decodeURIComponent(id.slice(id.lastIndexOf(':') + 1)).startsWith(input)));
|
|
258
|
+
if (matches.length > 1)
|
|
259
|
+
throw new CollaborationError('SESSION_ID_AMBIGUOUS', '成员短 ID 匹配多个会话,请使用完整 ID');
|
|
260
|
+
return matches[0] ?? null;
|
|
261
|
+
};
|
|
215
262
|
const groupView = (group) => {
|
|
216
263
|
const names = resolveNames?.(group.sessionIds) ?? {};
|
|
217
264
|
return {
|
|
@@ -220,6 +267,18 @@ export function collaborationRoutes({ store, resolveSession, deliver, rebind, re
|
|
|
220
267
|
roles: group.roles ?? {},
|
|
221
268
|
};
|
|
222
269
|
};
|
|
270
|
+
router.get('/rules', run((req, res, sessionId) => {
|
|
271
|
+
const group = groupOf(String(req.query.group ?? ''), sessionId);
|
|
272
|
+
res.json({ ok: true, group_id: group.id, instructions: group.instructions ?? null });
|
|
273
|
+
}));
|
|
274
|
+
router.post('/rules', run((req, res, sessionId) => {
|
|
275
|
+
const group = groupOf(String(req.body.group_id ?? ''), sessionId);
|
|
276
|
+
const expected = req.body.expected_version;
|
|
277
|
+
if (expected !== undefined && typeof expected !== 'string')
|
|
278
|
+
throw new Error('INVALID_VERSION');
|
|
279
|
+
const instructions = store.setRules(group.id, req.body.text, sessionId, expected);
|
|
280
|
+
res.json({ ok: true, group_id: group.id, instructions });
|
|
281
|
+
}));
|
|
223
282
|
router.get('/role', run((req, res, sessionId) => {
|
|
224
283
|
const groupId = String(req.query.group ?? '');
|
|
225
284
|
if (groupId) {
|
|
@@ -236,13 +295,14 @@ export function collaborationRoutes({ store, resolveSession, deliver, rebind, re
|
|
|
236
295
|
router.post('/role', run((req, res, sessionId) => {
|
|
237
296
|
const group = groupOf(String(req.body.group_id ?? ''), sessionId);
|
|
238
297
|
const role = req.body.role;
|
|
239
|
-
|
|
298
|
+
const member = typeof req.body.session_id === 'string' ? memberOf(group.sessionIds, req.body.session_id) : null;
|
|
299
|
+
if (!member) {
|
|
240
300
|
throw new CollaborationError('NOT_A_MEMBER', 'Role target must be a member of this group', 400);
|
|
241
301
|
}
|
|
242
302
|
if (role === undefined || (role !== null && typeof role !== 'string')) {
|
|
243
303
|
throw new CollaborationError('INVALID_ROLE', 'role must be text or null', 400);
|
|
244
304
|
}
|
|
245
|
-
const updated = store.setRole({ groupId: group.id, sessionId:
|
|
305
|
+
const updated = store.setRole({ groupId: group.id, sessionId: member, role: role });
|
|
246
306
|
res.json({ ok: true, group: { ...updated, roles: updated.roles ?? {} } });
|
|
247
307
|
}));
|
|
248
308
|
router.post('/name', run(async (req, res, sessionId) => {
|