termdock 1.4.350 → 1.4.352
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/AndroidMirrorView-DkOZxYLg.js +3 -0
- package/dist/client/assets/AndroidMirrorView-DkOZxYLg.js.br +0 -0
- package/dist/client/assets/AndroidMirrorView-DkOZxYLg.js.gz +0 -0
- package/dist/client/assets/App-DqRkfVSm.js +178 -0
- package/dist/client/assets/App-DqRkfVSm.js.br +0 -0
- package/dist/client/assets/App-DqRkfVSm.js.gz +0 -0
- package/dist/client/assets/ArchitecturePanel-m9GKl3cl.js +9 -0
- package/dist/client/assets/ArchitecturePanel-m9GKl3cl.js.br +0 -0
- package/dist/client/assets/ArchitecturePanel-m9GKl3cl.js.gz +0 -0
- package/dist/client/assets/ChangeWalkthroughPanel-HZ0zvFvi.js +2 -0
- package/dist/client/assets/ChangeWalkthroughPanel-HZ0zvFvi.js.br +0 -0
- package/dist/client/assets/ChangeWalkthroughPanel-HZ0zvFvi.js.gz +0 -0
- package/dist/client/assets/CollaborationMainWorkspace-B4OMk92m.js +1 -0
- package/dist/client/assets/CollaborationMainWorkspace-B4OMk92m.js.br +0 -0
- package/dist/client/assets/CollaborationMainWorkspace-B4OMk92m.js.gz +0 -0
- package/dist/client/assets/CollaborationTaskContent-Cbg3iH6E.js +2 -0
- package/dist/client/assets/ComputerControlView-CSs54lf0.js +6 -0
- package/dist/client/assets/ComputerControlView-CSs54lf0.js.br +0 -0
- package/dist/client/assets/ComputerControlView-CSs54lf0.js.gz +0 -0
- package/dist/client/assets/DagPlayground-yoS2llnB.js +1 -0
- package/dist/client/assets/DagPlayground-yoS2llnB.js.br +0 -0
- package/dist/client/assets/DagPlayground-yoS2llnB.js.gz +0 -0
- package/dist/client/assets/{DiffLab-DEw4y4Hk.js → DiffLab-Dj1MT9pY.js} +2 -2
- package/dist/client/assets/DiffLab-Dj1MT9pY.js.br +0 -0
- package/dist/client/assets/DiffLab-Dj1MT9pY.js.gz +0 -0
- package/dist/client/assets/DiffReview-BX2S9hNp.js +1 -0
- package/dist/client/assets/DiffReview-BX2S9hNp.js.br +0 -0
- package/dist/client/assets/DiffReview-BX2S9hNp.js.gz +0 -0
- package/dist/client/assets/{DiffReviewLab-CDXsy0jE.js → DiffReviewLab-DqI14GEk.js} +2 -2
- package/dist/client/assets/DiffReviewLab-DqI14GEk.js.br +0 -0
- package/dist/client/assets/DiffReviewLab-DqI14GEk.js.gz +0 -0
- package/dist/client/assets/DiffViewer-AZvdDGfH.js +34 -0
- package/dist/client/assets/DiffViewer-AZvdDGfH.js.br +0 -0
- package/dist/client/assets/DiffViewer-AZvdDGfH.js.gz +0 -0
- package/dist/client/assets/GitLoadingSkeleton-CBPykkZP.js +1 -0
- package/dist/client/assets/GitLoadingSkeleton-CBPykkZP.js.br +0 -0
- package/dist/client/assets/GitLoadingSkeleton-CBPykkZP.js.gz +0 -0
- package/dist/client/assets/MarkdownPreview-DpvTnKkj.js +19 -0
- package/dist/client/assets/MarkdownPreview-DpvTnKkj.js.br +0 -0
- package/dist/client/assets/MarkdownPreview-DpvTnKkj.js.gz +0 -0
- package/dist/client/assets/{ModelPreview-BEiOgE0G.js → ModelPreview-Du_AyPr9.js} +166 -166
- package/dist/client/assets/ModelPreview-Du_AyPr9.js.br +0 -0
- package/dist/client/assets/ModelPreview-Du_AyPr9.js.gz +0 -0
- package/dist/client/assets/RecordingSaveDialog-CtXaGgIo.js +1 -0
- package/dist/client/assets/RecordingSaveDialog-CtXaGgIo.js.br +0 -0
- package/dist/client/assets/RecordingSaveDialog-CtXaGgIo.js.gz +0 -0
- package/dist/client/assets/RightSidebar-DU3gwdzg.js +1662 -0
- package/dist/client/assets/RightSidebar-DU3gwdzg.js.br +0 -0
- package/dist/client/assets/RightSidebar-DU3gwdzg.js.gz +0 -0
- package/dist/client/assets/SessionChatView-CxfwJMuK.js +13 -0
- package/dist/client/assets/SessionChatView-CxfwJMuK.js.br +0 -0
- package/dist/client/assets/SessionChatView-CxfwJMuK.js.gz +0 -0
- package/dist/client/assets/SessionChatView-D8GoESjb.css +1 -0
- package/dist/client/assets/SessionChatView-D8GoESjb.css.br +0 -0
- package/dist/client/assets/SessionChatView-D8GoESjb.css.gz +0 -0
- package/dist/client/assets/SessionResourceSidebar-B31PU57J.js +2 -0
- package/dist/client/assets/SessionResourceSidebar-B31PU57J.js.br +0 -0
- package/dist/client/assets/SessionResourceSidebar-B31PU57J.js.gz +0 -0
- package/dist/client/assets/TodoWorkspace-j9BLgk94.js +1 -0
- package/dist/client/assets/TodoWorkspace-j9BLgk94.js.br +0 -0
- package/dist/client/assets/TodoWorkspace-j9BLgk94.js.gz +0 -0
- package/dist/client/assets/{abnfDiagram-VCTEODGH-ByzlUu_B.js → abnfDiagram-VCTEODGH-LfmMIpPX.js} +1 -1
- package/dist/client/assets/abnfDiagram-VCTEODGH-LfmMIpPX.js.br +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-LfmMIpPX.js.gz +0 -0
- package/dist/client/assets/app-BMJesFDU.js +36 -0
- package/dist/client/assets/app-BMJesFDU.js.br +0 -0
- package/dist/client/assets/app-BMJesFDU.js.gz +0 -0
- package/dist/client/assets/{arc-BA6JM6vl.js → arc-DMzcmH2U.js} +1 -1
- package/dist/client/assets/arc-DMzcmH2U.js.br +0 -0
- package/dist/client/assets/arc-DMzcmH2U.js.gz +0 -0
- package/dist/client/assets/{architectureDiagram-5GKGNRK7-hCHxOfKw.js → architectureDiagram-5GKGNRK7-CphS_S5Q.js} +1 -1
- package/dist/client/assets/architectureDiagram-5GKGNRK7-CphS_S5Q.js.br +0 -0
- package/dist/client/assets/{architectureDiagram-5GKGNRK7-hCHxOfKw.js.gz → architectureDiagram-5GKGNRK7-CphS_S5Q.js.gz} +0 -0
- package/dist/client/assets/arrow-down-D-WZS308.js +1 -0
- package/dist/client/assets/arrow-up-CelbB9FE.js +1 -0
- package/dist/client/assets/{blockDiagram-I7D4REHJ-OmEN9hUB.js → blockDiagram-I7D4REHJ-Dx_4qkcL.js} +1 -1
- package/dist/client/assets/blockDiagram-I7D4REHJ-Dx_4qkcL.js.br +0 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-Dx_4qkcL.js.gz +0 -0
- package/dist/client/assets/{c4Diagram-7LVT6UL2-DfLTv8zh.js → c4Diagram-7LVT6UL2-DiGdnPo4.js} +1 -1
- package/dist/client/assets/c4Diagram-7LVT6UL2-DiGdnPo4.js.br +0 -0
- package/dist/client/assets/c4Diagram-7LVT6UL2-DiGdnPo4.js.gz +0 -0
- package/dist/client/assets/channel-BxAJ9Qmh.js +1 -0
- package/dist/client/assets/chevron-left-5sk7gL3u.js +1 -0
- package/dist/client/assets/{chunk-2Q5K7J3B-C3O76PNZ.js → chunk-2Q5K7J3B-M50KNmR8.js} +1 -1
- package/dist/client/assets/{chunk-5VM5RSS4-CsWZd30k.js → chunk-5VM5RSS4-wXVM-Max.js} +1 -1
- package/dist/client/assets/{chunk-F27PBJKO-yxCv2Uqk.js → chunk-F27PBJKO-BfQILS9u.js} +1 -1
- package/dist/client/assets/chunk-F27PBJKO-BfQILS9u.js.br +0 -0
- package/dist/client/assets/chunk-F27PBJKO-BfQILS9u.js.gz +0 -0
- package/dist/client/assets/{chunk-IMKFNOWR-CdPjlb9f.js → chunk-IMKFNOWR-D111Jpl3.js} +1 -1
- package/dist/client/assets/chunk-IMKFNOWR-D111Jpl3.js.br +0 -0
- package/dist/client/assets/chunk-IMKFNOWR-D111Jpl3.js.gz +0 -0
- package/dist/client/assets/{chunk-JWPE2WC7-BiiOLq2R.js → chunk-JWPE2WC7-BBDyly5c.js} +1 -1
- package/dist/client/assets/{chunk-POPQ4Y6H-BOu_1Brt.js → chunk-POPQ4Y6H-BaI-_yc7.js} +1 -1
- package/dist/client/assets/{chunk-SVP7TREG-D1tOJung.js → chunk-SVP7TREG-BvQafCMJ.js} +1 -1
- package/dist/client/assets/chunk-SVP7TREG-BvQafCMJ.js.br +0 -0
- package/dist/client/assets/chunk-SVP7TREG-BvQafCMJ.js.gz +0 -0
- package/dist/client/assets/{chunk-TICWLB2K-DZjDfYNw.js → chunk-TICWLB2K-CQXPwGBz.js} +1 -1
- package/dist/client/assets/chunk-TICWLB2K-CQXPwGBz.js.br +0 -0
- package/dist/client/assets/chunk-TICWLB2K-CQXPwGBz.js.gz +0 -0
- package/dist/client/assets/{chunk-XXDRQBXY-CtGPyXWh.js → chunk-XXDRQBXY-CmwZc4xV.js} +1 -1
- package/dist/client/assets/classDiagram-ZZMXUADV-D0oXVFxd.js +1 -0
- package/dist/client/assets/classDiagram-v2-VYDZK3BY-D0oXVFxd.js +1 -0
- package/dist/client/assets/clipboard-uSv3AiJM.js +1 -0
- package/dist/client/assets/clipboardText-5puxqqm_.js +1 -0
- package/dist/client/assets/clipboardText-5puxqqm_.js.br +0 -0
- package/dist/client/assets/clipboardText-5puxqqm_.js.gz +0 -0
- package/dist/client/assets/{cose-bilkent-JH36ORCC-C4Wb5lqD.js → cose-bilkent-JH36ORCC-CqWarkUj.js} +1 -1
- package/dist/client/assets/cose-bilkent-JH36ORCC-CqWarkUj.js.br +0 -0
- package/dist/client/assets/cose-bilkent-JH36ORCC-CqWarkUj.js.gz +0 -0
- package/dist/client/assets/{cynefin-OW5HDTMX-BvWKB1Xk.js → cynefin-OW5HDTMX-CFW2ifig.js} +1 -1
- package/dist/client/assets/cynefin-OW5HDTMX-CFW2ifig.js.br +0 -0
- package/dist/client/assets/cynefin-OW5HDTMX-CFW2ifig.js.gz +0 -0
- package/dist/client/assets/{cynefinDiagram-5FMLGOSQ-7tAkHD2P.js → cynefinDiagram-5FMLGOSQ-yggxM55S.js} +1 -1
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-yggxM55S.js.br +0 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-yggxM55S.js.gz +0 -0
- package/dist/client/assets/{dagre-GXQ25YYZ-Bs6IPwks.js → dagre-GXQ25YYZ-inq7o--x.js} +1 -1
- package/dist/client/assets/dagre-GXQ25YYZ-inq7o--x.js.br +0 -0
- package/dist/client/assets/dagre-GXQ25YYZ-inq7o--x.js.gz +0 -0
- package/dist/client/assets/{diagram-S7CK7UJ4-CmYXaTya.js → diagram-S7CK7UJ4-BUXMnyQN.js} +1 -1
- package/dist/client/assets/diagram-S7CK7UJ4-BUXMnyQN.js.br +0 -0
- package/dist/client/assets/diagram-S7CK7UJ4-BUXMnyQN.js.gz +0 -0
- package/dist/client/assets/{diagram-UQ7AKVKN-CqxoefB7.js → diagram-UQ7AKVKN-BJbw-Imf.js} +1 -1
- package/dist/client/assets/diagram-UQ7AKVKN-BJbw-Imf.js.br +0 -0
- package/dist/client/assets/diagram-UQ7AKVKN-BJbw-Imf.js.gz +0 -0
- package/dist/client/assets/{diagram-VSXAHHWV-Dxp1MKPT.js → diagram-VSXAHHWV-CSfl8yS7.js} +1 -1
- package/dist/client/assets/diagram-VSXAHHWV-CSfl8yS7.js.br +0 -0
- package/dist/client/assets/diagram-VSXAHHWV-CSfl8yS7.js.gz +0 -0
- package/dist/client/assets/{diagram-VX7I27RA-BKbWvvEr.js → diagram-VX7I27RA-zV-Vrv89.js} +1 -1
- package/dist/client/assets/diagram-VX7I27RA-zV-Vrv89.js.br +0 -0
- package/dist/client/assets/{diagram-VX7I27RA-BKbWvvEr.js.gz → diagram-VX7I27RA-zV-Vrv89.js.gz} +0 -0
- package/dist/client/assets/{diagram-Z3DM3KII-BZnNaP7F.js → diagram-Z3DM3KII-CRnE-snw.js} +1 -1
- package/dist/client/assets/diagram-Z3DM3KII-CRnE-snw.js.br +0 -0
- package/dist/client/assets/diagram-Z3DM3KII-CRnE-snw.js.gz +0 -0
- package/dist/client/assets/{ebnfDiagram-PWID7BFC-BXVlN_P7.js → ebnfDiagram-PWID7BFC-CE30oImc.js} +1 -1
- package/dist/client/assets/ebnfDiagram-PWID7BFC-CE30oImc.js.br +0 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-CE30oImc.js.gz +0 -0
- package/dist/client/assets/{erDiagram-RLTQ6QDP-CEdYDZU7.js → erDiagram-RLTQ6QDP-D-5bIySI.js} +1 -1
- package/dist/client/assets/erDiagram-RLTQ6QDP-D-5bIySI.js.br +0 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-D-5bIySI.js.gz +0 -0
- package/dist/client/assets/{flowDiagram-HODETNUW-Bbw8GPCZ.js → flowDiagram-HODETNUW-E_jPKtQR.js} +1 -1
- package/dist/client/assets/flowDiagram-HODETNUW-E_jPKtQR.js.br +0 -0
- package/dist/client/assets/flowDiagram-HODETNUW-E_jPKtQR.js.gz +0 -0
- package/dist/client/assets/{ganttDiagram-EL5Y4UJY-CcFbtP2y.js → ganttDiagram-EL5Y4UJY-BWvLOUzC.js} +1 -1
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-BWvLOUzC.js.br +0 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-BWvLOUzC.js.gz +0 -0
- package/dist/client/assets/gestureArbiter-DszagC-d.js +1 -0
- package/dist/client/assets/gestureArbiter-DszagC-d.js.br +0 -0
- package/dist/client/assets/gestureArbiter-DszagC-d.js.gz +0 -0
- package/dist/client/assets/git-compare-BlcHEuBI.js +1 -0
- package/dist/client/assets/{gitGraphDiagram-WWUBYQGX-Eiust154.js → gitGraphDiagram-WWUBYQGX-nOlVCBmL.js} +1 -1
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-nOlVCBmL.js.br +0 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-nOlVCBmL.js.gz +0 -0
- package/dist/client/assets/index-CA1fR_jI.css +1 -0
- package/dist/client/assets/index-CA1fR_jI.css.br +0 -0
- package/dist/client/assets/index-CA1fR_jI.css.gz +0 -0
- package/dist/client/assets/{infoDiagram-27XIBGKW-DUmaNZJW.js → infoDiagram-27XIBGKW-CjVm9Lzz.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-5VMMS53U-qTwgXfx_.js → ishikawaDiagram-5VMMS53U-DmVMaGVp.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-5VMMS53U-qTwgXfx_.js.br → ishikawaDiagram-5VMMS53U-DmVMaGVp.js.br} +0 -0
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-DmVMaGVp.js.gz +0 -0
- package/dist/client/assets/{journeyDiagram-3NMN7TZE-DKy1msWo.js → journeyDiagram-3NMN7TZE-DLwkoAy9.js} +1 -1
- package/dist/client/assets/journeyDiagram-3NMN7TZE-DLwkoAy9.js.br +0 -0
- package/dist/client/assets/journeyDiagram-3NMN7TZE-DLwkoAy9.js.gz +0 -0
- package/dist/client/assets/{kanban-definition-UXKFOSKX-BxibybDd.js → kanban-definition-UXKFOSKX-GWkp_QiO.js} +1 -1
- package/dist/client/assets/kanban-definition-UXKFOSKX-GWkp_QiO.js.br +0 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-GWkp_QiO.js.gz +0 -0
- package/dist/client/assets/{layout-Bh6fKHRM.js → layout-DFg5a61y.js} +1 -1
- package/dist/client/assets/layout-DFg5a61y.js.br +0 -0
- package/dist/client/assets/layout-DFg5a61y.js.gz +0 -0
- package/dist/client/assets/{linear-SRlL3VWX.js → linear-B0OdJLe1.js} +1 -1
- package/dist/client/assets/{linear-SRlL3VWX.js.br → linear-B0OdJLe1.js.br} +0 -0
- package/dist/client/assets/linear-B0OdJLe1.js.gz +0 -0
- package/dist/client/assets/link-BnxdKJ7b.js +8 -0
- package/dist/client/assets/link-BnxdKJ7b.js.br +0 -0
- package/dist/client/assets/link-BnxdKJ7b.js.gz +0 -0
- package/dist/client/assets/list-tree-Bou8VSzi.js +1 -0
- package/dist/client/assets/loader-circle-q7h8uwFI.js +1 -0
- package/dist/client/assets/maximize-2-BGTBw-Ku.js +1 -0
- package/dist/client/assets/{mermaid.core-D0bcNdw3.js → mermaid.core-DQA3ykXv.js} +5 -5
- package/dist/client/assets/mermaid.core-DQA3ykXv.js.br +0 -0
- package/dist/client/assets/mermaid.core-DQA3ykXv.js.gz +0 -0
- package/dist/client/assets/{mindmap-definition-YA3MSWOX-CImZ_aI0.js → mindmap-definition-YA3MSWOX-DsZAVAGP.js} +2 -2
- package/dist/client/assets/mindmap-definition-YA3MSWOX-DsZAVAGP.js.br +0 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-DsZAVAGP.js.gz +0 -0
- package/dist/client/assets/passwordBootstrap-CEZSL94g.js +2 -0
- package/dist/client/assets/passwordBootstrap-CEZSL94g.js.br +0 -0
- package/dist/client/assets/passwordBootstrap-CEZSL94g.js.gz +0 -0
- package/dist/client/assets/{pegDiagram-XKGWAZYB-BT4pTf8O.js → pegDiagram-XKGWAZYB-C3ar01Lf.js} +1 -1
- package/dist/client/assets/pegDiagram-XKGWAZYB-C3ar01Lf.js.br +0 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-C3ar01Lf.js.gz +0 -0
- package/dist/client/assets/{pieDiagram-E7YTZNPT-C5K58ien.js → pieDiagram-E7YTZNPT-C8LIa9gE.js} +1 -1
- package/dist/client/assets/pieDiagram-E7YTZNPT-C8LIa9gE.js.br +0 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-C8LIa9gE.js.gz +0 -0
- package/dist/client/assets/{quadrantDiagram-AXDQQJYC-DzSugYXz.js → quadrantDiagram-AXDQQJYC-BuRJ1f9F.js} +1 -1
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-BuRJ1f9F.js.br +0 -0
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-BuRJ1f9F.js.gz +0 -0
- package/dist/client/assets/quote-qXNI_NTi.js +1 -0
- package/dist/client/assets/{railroadDiagram-O6MQD6OU-BY07SouQ.js → railroadDiagram-O6MQD6OU-D7iZy3dO.js} +1 -1
- package/dist/client/assets/railroadDiagram-O6MQD6OU-D7iZy3dO.js.br +0 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-D7iZy3dO.js.gz +0 -0
- package/dist/client/assets/rdpSession-DUBlG26K.js +1 -0
- package/dist/client/assets/rdpSession-DUBlG26K.js.br +0 -0
- package/dist/client/assets/rdpSession-DUBlG26K.js.gz +0 -0
- package/dist/client/assets/{requirementDiagram-BXWQKSXE-C7WieVuE.js → requirementDiagram-BXWQKSXE-DhY8g8qK.js} +1 -1
- package/dist/client/assets/requirementDiagram-BXWQKSXE-DhY8g8qK.js.br +0 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-DhY8g8qK.js.gz +0 -0
- package/dist/client/assets/{sankeyDiagram-P5KCCOFB-BJsDWNs4.js → sankeyDiagram-P5KCCOFB-DDA_LQcg.js} +1 -1
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-DDA_LQcg.js.br +0 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-DDA_LQcg.js.gz +0 -0
- package/dist/client/assets/{sequenceDiagram-WJ2MYXX4-BeJ_7853.js → sequenceDiagram-WJ2MYXX4-bWgfdfwY.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-WJ2MYXX4-BeJ_7853.js.br → sequenceDiagram-WJ2MYXX4-bWgfdfwY.js.br} +0 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-bWgfdfwY.js.gz +0 -0
- package/dist/client/assets/settings-2-B-sr3wQM.js +1 -0
- package/dist/client/assets/{sizeCapture-INFHLROL-DOsjXKPV.js → sizeCapture-INFHLROL-BYxi7e0R.js} +1 -1
- package/dist/client/assets/sizeCapture-INFHLROL-BYxi7e0R.js.br +0 -0
- package/dist/client/assets/sizeCapture-INFHLROL-BYxi7e0R.js.gz +0 -0
- package/dist/client/assets/{sparkles-e9SkpP7a.js → sparkles-CCLflSFy.js} +1 -1
- package/dist/client/assets/{stateDiagram-D77RDMKH-6s709_RT.js → stateDiagram-D77RDMKH-6vqC2h36.js} +1 -1
- package/dist/client/assets/stateDiagram-D77RDMKH-6vqC2h36.js.br +0 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-6vqC2h36.js.gz +0 -0
- package/dist/client/assets/stateDiagram-v2-MP3YSRHH-Bb23lB9W.js +1 -0
- package/dist/client/assets/sun-B9_TfxlA.js +1 -0
- package/dist/client/assets/{swimlanes-42K2YHIH-5R5dw9_R.js → swimlanes-42K2YHIH-B4qrXam7.js} +1 -1
- package/dist/client/assets/swimlanes-42K2YHIH-B4qrXam7.js.br +0 -0
- package/dist/client/assets/swimlanes-42K2YHIH-B4qrXam7.js.gz +0 -0
- package/dist/client/assets/swimlanesDiagram-VR7AAH4N-CQ1XJUVU.js +8 -0
- package/dist/client/assets/swiper-DJjq1TYT.js +1 -0
- package/dist/client/assets/swiper-DJjq1TYT.js.br +0 -0
- package/dist/client/assets/swiper-DJjq1TYT.js.gz +0 -0
- package/dist/client/assets/syntaxHighlight-bcOB6oww.js +4 -0
- package/dist/client/assets/syntaxHighlight-bcOB6oww.js.br +0 -0
- package/dist/client/assets/syntaxHighlight-bcOB6oww.js.gz +0 -0
- package/dist/client/assets/{timeline-definition-24CTP7MA-B4YN0lsx.js → timeline-definition-24CTP7MA-D0_004MF.js} +1 -1
- package/dist/client/assets/timeline-definition-24CTP7MA-D0_004MF.js.br +0 -0
- package/dist/client/assets/timeline-definition-24CTP7MA-D0_004MF.js.gz +0 -0
- package/dist/client/assets/triangle-alert-D3w7mA17.js +1 -0
- package/dist/client/assets/unplug-BSblgecr.js +1 -0
- package/dist/client/assets/use-gesture-react.esm-DYpdiE_k.js +1 -0
- package/dist/client/assets/use-gesture-react.esm-DYpdiE_k.js.br +0 -0
- package/dist/client/assets/use-gesture-react.esm-DYpdiE_k.js.gz +0 -0
- package/dist/client/assets/v4-EwEgHOG0.js +1 -0
- package/dist/client/assets/{vennDiagram-4TSXK5OY-Wd83SmWI.js → vennDiagram-4TSXK5OY-Bqrx-47p.js} +1 -1
- package/dist/client/assets/vennDiagram-4TSXK5OY-Bqrx-47p.js.br +0 -0
- package/dist/client/assets/vennDiagram-4TSXK5OY-Bqrx-47p.js.gz +0 -0
- package/dist/client/assets/videoCompression-df1Ej3AR.js +3 -0
- package/dist/client/assets/videoCompression-df1Ej3AR.js.br +0 -0
- package/dist/client/assets/videoCompression-df1Ej3AR.js.gz +0 -0
- package/dist/client/assets/{wardleyDiagram-VM6X3IG4-CGsYvqWN.js → wardleyDiagram-VM6X3IG4-C4LCUdfd.js} +1 -1
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-C4LCUdfd.js.br +0 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-C4LCUdfd.js.gz +0 -0
- package/dist/client/assets/{xychartDiagram-S5SC5T6Z-ednJq1_u.js → xychartDiagram-S5SC5T6Z-E5AQcZVI.js} +1 -1
- package/dist/client/assets/{xychartDiagram-S5SC5T6Z-ednJq1_u.js.br → xychartDiagram-S5SC5T6Z-E5AQcZVI.js.br} +0 -0
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-E5AQcZVI.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-encrypted-background.js +1 -1
- package/dist/client/sw-encrypted-background.js.br +0 -0
- package/dist/client/sw-encrypted-background.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/collaborationDeliveryWorker.js +11 -0
- package/dist/server/agent/collaborationPrompt.js +4 -2
- package/dist/server/agent/collaborationRoutes.js +2 -0
- package/dist/server/agent/collaborationStore.js +7 -1
- package/dist/server/agent/sessionAttachmentRoutes.js +39 -0
- package/dist/server/agent/sessionChatRoutes.js +64 -0
- package/dist/server/agent/sessionViewModeRoutes.js +72 -0
- package/dist/server/android/recording.js +19 -17
- package/dist/server/cli.js +34 -0
- package/dist/server/entry.js +26 -17
- package/dist/server/federation/accessPolicy.js +8 -3
- package/dist/server/federation/authorization.js +26 -3
- package/dist/server/federation/invitations.js +27 -6
- package/dist/server/federation/relay.js +21 -3
- package/dist/server/federation/requestContext.js +5 -1
- package/dist/server/federation/resourceAccess.js +63 -0
- package/dist/server/federation/runtime.js +67 -18
- package/dist/server/federation/sessionSharing.js +39 -0
- package/dist/server/federation/socketDuplex.js +3 -3
- package/dist/server/routes/android.js +36 -7
- package/dist/server/routes/computer.js +14 -4
- package/dist/server/routes/filesystem.js +6 -3
- package/dist/server/routes/rdp.js +4 -3
- package/dist/server/routes/sharedFiles.js +51 -0
- package/dist/server/routes/terminal.js +83 -14
- package/dist/server/routes/vncReadOnly.js +134 -0
- package/dist/server/shared/shareResources.js +37 -0
- package/dist/server/todos/cli.js +91 -0
- package/dist/server/todos/routes.js +73 -0
- package/dist/server/todos/store.js +240 -0
- package/dist/server/todos/types.js +1 -0
- package/dist/server/utils/autoUpdate.js +7 -2
- package/dist/server/utils/clientSnapshotMaintenance.js +200 -0
- package/dist/server/utils/runtimeClient.js +79 -0
- package/dist/server/utils/serverCleanup.js +24 -0
- package/dist/server/utils/storageMaintenance.js +60 -0
- package/dist/server/utils/version.js +2 -0
- package/dist/server/utils/workspaceCacheMaintenance.js +158 -0
- package/docs/integrations.md +23 -0
- package/package.json +1 -1
- package/runtime-manifest.json +3 -3
- package/dist/client/assets/App-TPJoJbGD.js +0 -187
- package/dist/client/assets/App-TPJoJbGD.js.br +0 -0
- package/dist/client/assets/App-TPJoJbGD.js.gz +0 -0
- package/dist/client/assets/ArchitecturePanel-DzVy71gM.js +0 -9
- package/dist/client/assets/ArchitecturePanel-DzVy71gM.js.br +0 -0
- package/dist/client/assets/ArchitecturePanel-DzVy71gM.js.gz +0 -0
- package/dist/client/assets/ChangeWalkthroughPanel-DN7BCzqC.js +0 -2
- package/dist/client/assets/ChangeWalkthroughPanel-DN7BCzqC.js.br +0 -0
- package/dist/client/assets/ChangeWalkthroughPanel-DN7BCzqC.js.gz +0 -0
- package/dist/client/assets/CollaborationMainWorkspace-B1PZuCC6.js +0 -1
- package/dist/client/assets/CollaborationMainWorkspace-B1PZuCC6.js.br +0 -0
- package/dist/client/assets/CollaborationMainWorkspace-B1PZuCC6.js.gz +0 -0
- package/dist/client/assets/CollaborationTaskContent-CG7g1LMH.js +0 -2
- package/dist/client/assets/ComputerControlView-BlI9Ai_k.js +0 -6
- package/dist/client/assets/ComputerControlView-BlI9Ai_k.js.br +0 -0
- package/dist/client/assets/ComputerControlView-BlI9Ai_k.js.gz +0 -0
- package/dist/client/assets/DagPlayground-V0hTKB-S.js +0 -1
- package/dist/client/assets/DagPlayground-V0hTKB-S.js.br +0 -0
- package/dist/client/assets/DagPlayground-V0hTKB-S.js.gz +0 -0
- package/dist/client/assets/DiffLab-DEw4y4Hk.js.br +0 -0
- package/dist/client/assets/DiffLab-DEw4y4Hk.js.gz +0 -0
- package/dist/client/assets/DiffReview-Cnnt3TMK.js +0 -1
- package/dist/client/assets/DiffReview-Cnnt3TMK.js.br +0 -0
- package/dist/client/assets/DiffReview-Cnnt3TMK.js.gz +0 -0
- package/dist/client/assets/DiffReviewLab-CDXsy0jE.js.br +0 -0
- package/dist/client/assets/DiffReviewLab-CDXsy0jE.js.gz +0 -0
- package/dist/client/assets/DiffViewer-Dh40XiCM.js +0 -37
- package/dist/client/assets/DiffViewer-Dh40XiCM.js.br +0 -0
- package/dist/client/assets/DiffViewer-Dh40XiCM.js.gz +0 -0
- package/dist/client/assets/ModelPreview-BEiOgE0G.js.br +0 -0
- package/dist/client/assets/ModelPreview-BEiOgE0G.js.gz +0 -0
- package/dist/client/assets/RightSidebar-DiECnX5D.js +0 -1679
- package/dist/client/assets/RightSidebar-DiECnX5D.js.br +0 -0
- package/dist/client/assets/RightSidebar-DiECnX5D.js.gz +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-ByzlUu_B.js.br +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-ByzlUu_B.js.gz +0 -0
- package/dist/client/assets/app-CKAFlgMD.js +0 -35
- package/dist/client/assets/app-CKAFlgMD.js.br +0 -0
- package/dist/client/assets/app-CKAFlgMD.js.gz +0 -0
- package/dist/client/assets/arc-BA6JM6vl.js.br +0 -0
- package/dist/client/assets/arc-BA6JM6vl.js.gz +0 -0
- package/dist/client/assets/architectureDiagram-5GKGNRK7-hCHxOfKw.js.br +0 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-OmEN9hUB.js.br +0 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-OmEN9hUB.js.gz +0 -0
- package/dist/client/assets/c4Diagram-7LVT6UL2-DfLTv8zh.js.br +0 -0
- package/dist/client/assets/c4Diagram-7LVT6UL2-DfLTv8zh.js.gz +0 -0
- package/dist/client/assets/channel-gcmN0N0m.js +0 -1
- package/dist/client/assets/chunk-F27PBJKO-yxCv2Uqk.js.br +0 -0
- package/dist/client/assets/chunk-F27PBJKO-yxCv2Uqk.js.gz +0 -0
- package/dist/client/assets/chunk-IMKFNOWR-CdPjlb9f.js.br +0 -0
- package/dist/client/assets/chunk-IMKFNOWR-CdPjlb9f.js.gz +0 -0
- package/dist/client/assets/chunk-SVP7TREG-D1tOJung.js.br +0 -0
- package/dist/client/assets/chunk-SVP7TREG-D1tOJung.js.gz +0 -0
- package/dist/client/assets/chunk-TICWLB2K-DZjDfYNw.js.br +0 -0
- package/dist/client/assets/chunk-TICWLB2K-DZjDfYNw.js.gz +0 -0
- package/dist/client/assets/classDiagram-ZZMXUADV-k4BRymfG.js +0 -1
- package/dist/client/assets/classDiagram-v2-VYDZK3BY-k4BRymfG.js +0 -1
- package/dist/client/assets/cose-bilkent-JH36ORCC-C4Wb5lqD.js.br +0 -0
- package/dist/client/assets/cose-bilkent-JH36ORCC-C4Wb5lqD.js.gz +0 -0
- package/dist/client/assets/cynefin-OW5HDTMX-BvWKB1Xk.js.br +0 -0
- package/dist/client/assets/cynefin-OW5HDTMX-BvWKB1Xk.js.gz +0 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-7tAkHD2P.js.br +0 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-7tAkHD2P.js.gz +0 -0
- package/dist/client/assets/dagre-GXQ25YYZ-Bs6IPwks.js.br +0 -0
- package/dist/client/assets/dagre-GXQ25YYZ-Bs6IPwks.js.gz +0 -0
- package/dist/client/assets/diagram-S7CK7UJ4-CmYXaTya.js.br +0 -0
- package/dist/client/assets/diagram-S7CK7UJ4-CmYXaTya.js.gz +0 -0
- package/dist/client/assets/diagram-UQ7AKVKN-CqxoefB7.js.br +0 -0
- package/dist/client/assets/diagram-UQ7AKVKN-CqxoefB7.js.gz +0 -0
- package/dist/client/assets/diagram-VSXAHHWV-Dxp1MKPT.js.br +0 -0
- package/dist/client/assets/diagram-VSXAHHWV-Dxp1MKPT.js.gz +0 -0
- package/dist/client/assets/diagram-VX7I27RA-BKbWvvEr.js.br +0 -0
- package/dist/client/assets/diagram-Z3DM3KII-BZnNaP7F.js.br +0 -0
- package/dist/client/assets/diagram-Z3DM3KII-BZnNaP7F.js.gz +0 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-BXVlN_P7.js.br +0 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-BXVlN_P7.js.gz +0 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-CEdYDZU7.js.br +0 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-CEdYDZU7.js.gz +0 -0
- package/dist/client/assets/flowDiagram-HODETNUW-Bbw8GPCZ.js.br +0 -0
- package/dist/client/assets/flowDiagram-HODETNUW-Bbw8GPCZ.js.gz +0 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-CcFbtP2y.js.br +0 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-CcFbtP2y.js.gz +0 -0
- package/dist/client/assets/gestureArbiter-sdzs-66-.js +0 -1
- package/dist/client/assets/gestureArbiter-sdzs-66-.js.br +0 -0
- package/dist/client/assets/gestureArbiter-sdzs-66-.js.gz +0 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-Eiust154.js.br +0 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-Eiust154.js.gz +0 -0
- package/dist/client/assets/index-DtPsPZ7W.css +0 -1
- package/dist/client/assets/index-DtPsPZ7W.css.br +0 -0
- package/dist/client/assets/index-DtPsPZ7W.css.gz +0 -0
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-qTwgXfx_.js.gz +0 -0
- package/dist/client/assets/journeyDiagram-3NMN7TZE-DKy1msWo.js.br +0 -0
- package/dist/client/assets/journeyDiagram-3NMN7TZE-DKy1msWo.js.gz +0 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-BxibybDd.js.br +0 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-BxibybDd.js.gz +0 -0
- package/dist/client/assets/layout-Bh6fKHRM.js.br +0 -0
- package/dist/client/assets/layout-Bh6fKHRM.js.gz +0 -0
- package/dist/client/assets/linear-SRlL3VWX.js.gz +0 -0
- package/dist/client/assets/mermaid.core-D0bcNdw3.js.br +0 -0
- package/dist/client/assets/mermaid.core-D0bcNdw3.js.gz +0 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-CImZ_aI0.js.br +0 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-CImZ_aI0.js.gz +0 -0
- package/dist/client/assets/passwordBootstrap-f7-mFb1b.js +0 -2
- package/dist/client/assets/passwordBootstrap-f7-mFb1b.js.br +0 -0
- package/dist/client/assets/passwordBootstrap-f7-mFb1b.js.gz +0 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-BT4pTf8O.js.br +0 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-BT4pTf8O.js.gz +0 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-C5K58ien.js.br +0 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-C5K58ien.js.gz +0 -0
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-DzSugYXz.js.br +0 -0
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-DzSugYXz.js.gz +0 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-BY07SouQ.js.br +0 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-BY07SouQ.js.gz +0 -0
- package/dist/client/assets/rdpSession-RNjO6yLw.js +0 -1
- package/dist/client/assets/rdpSession-RNjO6yLw.js.br +0 -0
- package/dist/client/assets/rdpSession-RNjO6yLw.js.gz +0 -0
- package/dist/client/assets/referenceLongPress-B9K9yPKR.js +0 -1
- package/dist/client/assets/referenceLongPress-B9K9yPKR.js.br +0 -0
- package/dist/client/assets/referenceLongPress-B9K9yPKR.js.gz +0 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-C7WieVuE.js.br +0 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-C7WieVuE.js.gz +0 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-BJsDWNs4.js.br +0 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-BJsDWNs4.js.gz +0 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-BeJ_7853.js.gz +0 -0
- package/dist/client/assets/sizeCapture-INFHLROL-DOsjXKPV.js.br +0 -0
- package/dist/client/assets/sizeCapture-INFHLROL-DOsjXKPV.js.gz +0 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-6s709_RT.js.br +0 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-6s709_RT.js.gz +0 -0
- package/dist/client/assets/stateDiagram-v2-MP3YSRHH-B43A9Hsz.js +0 -1
- package/dist/client/assets/swimlanes-42K2YHIH-5R5dw9_R.js.br +0 -0
- package/dist/client/assets/swimlanes-42K2YHIH-5R5dw9_R.js.gz +0 -0
- package/dist/client/assets/swimlanesDiagram-VR7AAH4N-DoFbqN_4.js +0 -8
- package/dist/client/assets/timeline-definition-24CTP7MA-B4YN0lsx.js.br +0 -0
- package/dist/client/assets/timeline-definition-24CTP7MA-B4YN0lsx.js.gz +0 -0
- package/dist/client/assets/triangle-alert-BzBDJvJX.js +0 -1
- package/dist/client/assets/vennDiagram-4TSXK5OY-Wd83SmWI.js.br +0 -0
- package/dist/client/assets/vennDiagram-4TSXK5OY-Wd83SmWI.js.gz +0 -0
- package/dist/client/assets/videoCompression-Bsw--tMn.js +0 -3
- package/dist/client/assets/videoCompression-Bsw--tMn.js.br +0 -0
- package/dist/client/assets/videoCompression-Bsw--tMn.js.gz +0 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-CGsYvqWN.js.br +0 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-CGsYvqWN.js.gz +0 -0
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-ednJq1_u.js.gz +0 -0
- /package/dist/client/assets/{gestureArbiter-C2J0D0kV.css → swiper-C2J0D0kV.css} +0 -0
- /package/dist/client/assets/{gestureArbiter-C2J0D0kV.css.br → swiper-C2J0D0kV.css.br} +0 -0
- /package/dist/client/assets/{gestureArbiter-C2J0D0kV.css.gz → swiper-C2J0D0kV.css.gz} +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validShareResources, resourceSubset } from '../shared/shareResources.js';
|
|
1
2
|
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { mkdirSync, readFileSync, openSync, writeFileSync, fsyncSync, closeSync, renameSync, unlinkSync } from 'node:fs';
|
|
3
4
|
import { dirname } from 'node:path';
|
|
@@ -18,6 +19,7 @@ function validateInput(v) {
|
|
|
18
19
|
assert(v.scope.kind === 'sessions' && ids(v.scope.sessionIds), 'session scope');
|
|
19
20
|
keys(v.scope, ['kind', 'sessionIds']);
|
|
20
21
|
}
|
|
22
|
+
assert(v.resources === undefined || validShareResources(v.resources), 'shared resources');
|
|
21
23
|
assert(Array.isArray(v.actions) && v.actions.length > 0 && v.actions.length <= 256 && v.actions.every(action) && new Set(v.actions).size === v.actions.length, 'actions');
|
|
22
24
|
if (v.scope.kind === 'sessions')
|
|
23
25
|
assert(v.actions.every(a => a.startsWith('session.')), 'session scope cannot grant service or file capabilities');
|
|
@@ -37,7 +39,7 @@ function isSubset(child, parent) {
|
|
|
37
39
|
return false;
|
|
38
40
|
if (parent.scope.kind === 'sessions' && (child.scope.kind !== 'sessions' || !child.scope.sessionIds.every(s => parent.scope.kind === 'sessions' && parent.scope.sessionIds.includes(s))))
|
|
39
41
|
return false;
|
|
40
|
-
if (!parent.actions.includes('service:*') && !child.actions.every(a => parent.actions.includes(a)))
|
|
42
|
+
if (!parent.actions.includes('service:*') && (!child.actions.every(a => parent.actions.includes(a)) || !resourceSubset(child.resources, parent.resources)))
|
|
41
43
|
return false;
|
|
42
44
|
for (const key of ['entryServiceIds', 'relayIds']) {
|
|
43
45
|
const limit = parent.conditions?.[key];
|
|
@@ -86,7 +88,7 @@ export class AuthorizationStore {
|
|
|
86
88
|
for (const value of data.grants) {
|
|
87
89
|
validateInput(value);
|
|
88
90
|
const g = value;
|
|
89
|
-
keys(g, ['id', 'serviceId', 'createdAt', 'parentGrantId', 'revokedAt', 'subjectId', 'scope', 'actions', 'expiresAt', 'canDelegate', 'conditions', 'passwordCredential']);
|
|
91
|
+
keys(g, ['id', 'serviceId', 'createdAt', 'parentGrantId', 'revokedAt', 'subjectId', 'scope', 'actions', 'expiresAt', 'canDelegate', 'conditions', 'passwordCredential', 'resources']);
|
|
90
92
|
assert(id(g.id) && g.serviceId === this.options.serviceId && Number.isSafeInteger(g.createdAt) && g.createdAt >= 0, 'grant metadata');
|
|
91
93
|
assert(g.parentGrantId === undefined || id(g.parentGrantId), 'parent identity');
|
|
92
94
|
assert(g.revokedAt === undefined || (Number.isSafeInteger(g.revokedAt) && g.revokedAt >= 0), 'revocation');
|
|
@@ -122,7 +124,7 @@ export class AuthorizationStore {
|
|
|
122
124
|
}
|
|
123
125
|
create(input) {
|
|
124
126
|
validateInput(input);
|
|
125
|
-
keys(input, ['subjectId', 'scope', 'actions', 'expiresAt', 'canDelegate', 'conditions']);
|
|
127
|
+
keys(input, ['subjectId', 'scope', 'actions', 'expiresAt', 'canDelegate', 'conditions', 'resources']);
|
|
126
128
|
const now = this.now();
|
|
127
129
|
assert(input.expiresAt === undefined || input.expiresAt > now, 'already expired');
|
|
128
130
|
return { ...structuredClone(input), id: randomUUID(), serviceId: this.options.serviceId, createdAt: now };
|
|
@@ -169,6 +171,27 @@ export class AuthorizationStore {
|
|
|
169
171
|
return true;
|
|
170
172
|
}
|
|
171
173
|
list() { return this.read(); }
|
|
174
|
+
listActive() { const grants = this.read(); return grants.filter(grant => active(grant, grants, this.now(), new Set(), this.options.passwordCredential?.())); }
|
|
175
|
+
/** Narrow only this Session, preserving sibling Session grants and descendant checks. */
|
|
176
|
+
revokeSession(sessionId, normalize = id => id) {
|
|
177
|
+
const grants = this.read();
|
|
178
|
+
let changed = 0;
|
|
179
|
+
for (const grant of grants) {
|
|
180
|
+
if (grant.revokedAt !== undefined || grant.scope.kind !== 'sessions')
|
|
181
|
+
continue;
|
|
182
|
+
const remaining = grant.scope.sessionIds.filter(id => normalize(id) !== sessionId);
|
|
183
|
+
if (remaining.length === grant.scope.sessionIds.length)
|
|
184
|
+
continue;
|
|
185
|
+
changed++;
|
|
186
|
+
if (!remaining.length)
|
|
187
|
+
grant.revokedAt = this.now();
|
|
188
|
+
else
|
|
189
|
+
grant.scope = { kind: 'sessions', sessionIds: remaining };
|
|
190
|
+
}
|
|
191
|
+
if (changed)
|
|
192
|
+
this.save(grants);
|
|
193
|
+
return changed;
|
|
194
|
+
}
|
|
172
195
|
listEffective(request) {
|
|
173
196
|
if (!request || !id(request.subjectId) || request.serviceId !== this.options.serviceId)
|
|
174
197
|
return [];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validShareResources } from '../shared/shareResources.js';
|
|
1
2
|
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
2
3
|
import { mkdirSync, readFileSync, openSync, writeFileSync, fsyncSync, closeSync, renameSync, unlinkSync, chmodSync } from 'node:fs';
|
|
3
4
|
import { dirname } from 'node:path';
|
|
@@ -8,8 +9,9 @@ const record = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
|
8
9
|
function requireValid(ok) { if (!ok)
|
|
9
10
|
throw new Error('INVALID_INVITATION'); }
|
|
10
11
|
export function validateInvitation(input) {
|
|
11
|
-
requireValid(record(input) && Object.keys(input).every(k => ['scope', 'actions', 'label', 'expiresAt'].includes(k)));
|
|
12
|
+
requireValid(record(input) && Object.keys(input).every(k => ['scope', 'actions', 'label', 'expiresAt', 'resources'].includes(k)));
|
|
12
13
|
requireValid(record(input.scope) && Array.isArray(input.actions) && input.actions.length > 0 && new Set(input.actions).size === input.actions.length);
|
|
14
|
+
requireValid(input.resources === undefined || validShareResources(input.resources));
|
|
13
15
|
const scope = input.scope;
|
|
14
16
|
if (scope.kind === 'service') {
|
|
15
17
|
requireValid(Object.keys(scope).length === 1 && ((input.actions.length === 1 && input.actions[0] === 'service:*') || input.actions.every(a => typeof a === 'string' && actions.has(a))));
|
|
@@ -50,9 +52,9 @@ export class InvitationStore {
|
|
|
50
52
|
requireValid(record(data) && data.version === 1 && data.serviceId === this.options.serviceId && Array.isArray(data.invitations) && data.invitations.length <= 2048);
|
|
51
53
|
const seen = new Set();
|
|
52
54
|
for (const item of data.invitations) {
|
|
53
|
-
requireValid(record(item) && Object.keys(item).every(k => ['scope', 'actions', 'label', 'expiresAt', 'hash', 'issuerId', 'createdAt', 'invitationExpiresAt', 'consumedAt', 'consumedBy'].includes(k)));
|
|
54
|
-
const { scope, actions, label, expiresAt, hash, issuerId, createdAt, invitationExpiresAt, consumedAt, consumedBy } = item;
|
|
55
|
-
validateInvitation({ scope, actions, ...(label === undefined ? {} : { label }), ...(expiresAt === undefined ? {} : { expiresAt }) });
|
|
55
|
+
requireValid(record(item) && Object.keys(item).every(k => ['scope', 'actions', 'label', 'expiresAt', 'hash', 'issuerId', 'createdAt', 'invitationExpiresAt', 'consumedAt', 'consumedBy', 'resources'].includes(k)));
|
|
56
|
+
const { scope, actions, resources, label, expiresAt, hash, issuerId, createdAt, invitationExpiresAt, consumedAt, consumedBy } = item;
|
|
57
|
+
validateInvitation({ scope, actions, ...(resources === undefined ? {} : { resources }), ...(label === undefined ? {} : { label }), ...(expiresAt === undefined ? {} : { expiresAt }) });
|
|
56
58
|
requireValid(typeof hash === 'string' && /^[a-f0-9]{64}$/.test(hash) && !seen.has(hash) && identity(issuerId));
|
|
57
59
|
seen.add(hash);
|
|
58
60
|
requireValid(Number.isSafeInteger(createdAt) && Number.isSafeInteger(invitationExpiresAt) && Number(invitationExpiresAt) === Number(createdAt) + TTL);
|
|
@@ -87,7 +89,7 @@ export class InvitationStore {
|
|
|
87
89
|
listPending() {
|
|
88
90
|
const now = this.now();
|
|
89
91
|
return this.read().filter(i => i.consumedAt === undefined && i.invitationExpiresAt > now && (i.expiresAt === undefined || i.expiresAt > now))
|
|
90
|
-
.map(({ hash, scope, actions, label, expiresAt, createdAt, invitationExpiresAt }) => ({ id: hash, scope, actions, ...(label ? { label } : {}), ...(expiresAt === undefined ? {} : { expiresAt }), createdAt, invitationExpiresAt }));
|
|
92
|
+
.map(({ hash, scope, actions, resources, label, expiresAt, createdAt, invitationExpiresAt }) => ({ id: hash, scope, actions, ...(resources === undefined ? {} : { resources }), ...(label ? { label } : {}), ...(expiresAt === undefined ? {} : { expiresAt }), createdAt, invitationExpiresAt }));
|
|
91
93
|
}
|
|
92
94
|
revoke(id) {
|
|
93
95
|
const current = this.read();
|
|
@@ -98,6 +100,25 @@ export class InvitationStore {
|
|
|
98
100
|
this.save(current);
|
|
99
101
|
return true;
|
|
100
102
|
}
|
|
103
|
+
revokeSession(sessionId, normalize = id => id) {
|
|
104
|
+
const current = this.read();
|
|
105
|
+
let changed = 0;
|
|
106
|
+
const next = current.filter(invitation => {
|
|
107
|
+
if (invitation.consumedAt !== undefined || invitation.scope.kind !== 'sessions')
|
|
108
|
+
return true;
|
|
109
|
+
const remaining = invitation.scope.sessionIds.filter(id => normalize(id) !== sessionId);
|
|
110
|
+
if (remaining.length === invitation.scope.sessionIds.length)
|
|
111
|
+
return true;
|
|
112
|
+
changed++;
|
|
113
|
+
if (!remaining.length)
|
|
114
|
+
return false;
|
|
115
|
+
invitation.scope = { kind: 'sessions', sessionIds: remaining };
|
|
116
|
+
return true;
|
|
117
|
+
});
|
|
118
|
+
if (changed)
|
|
119
|
+
this.save(next);
|
|
120
|
+
return changed;
|
|
121
|
+
}
|
|
101
122
|
create(issuerId, input) {
|
|
102
123
|
requireValid(identity(issuerId));
|
|
103
124
|
validateInvitation(input);
|
|
@@ -129,7 +150,7 @@ export class InvitationStore {
|
|
|
129
150
|
invite.consumedAt = now;
|
|
130
151
|
invite.consumedBy = subjectId;
|
|
131
152
|
this.save(current);
|
|
132
|
-
return { subjectId, scope: structuredClone(invite.scope), actions: [...invite.actions], ...(invite.expiresAt === undefined ? {} : { expiresAt: invite.expiresAt }), canDelegate: false };
|
|
153
|
+
return { subjectId, ...(invite.resources ? { resources: structuredClone(invite.resources) } : {}), scope: structuredClone(invite.scope), actions: [...invite.actions], ...(invite.expiresAt === undefined ? {} : { expiresAt: invite.expiresAt }), canDelegate: false };
|
|
133
154
|
}
|
|
134
155
|
subjectLabels() {
|
|
135
156
|
return Object.fromEntries(this.read().filter(i => i.consumedBy && i.label).map(i => [i.consumedBy, i.label]));
|
|
@@ -211,7 +211,8 @@ export class RelayRouter {
|
|
|
211
211
|
s.provider.streams.delete(s.providerId);
|
|
212
212
|
send(s.provider.ws, { type: 'close', streamId: s.providerId, reason }, this.limits);
|
|
213
213
|
}
|
|
214
|
-
send(s.owner.ws, { type: 'close', streamId: s.ownerId, reason
|
|
214
|
+
send(s.owner.ws, { type: 'close', streamId: s.ownerId, reason,
|
|
215
|
+
...(!this.canRoute(s.owner, s.serviceId) ? { code: 'RELAY_AUTHORIZATION_REQUIRED' } : {}) }, this.limits);
|
|
215
216
|
}
|
|
216
217
|
providerFailed(s, reason) {
|
|
217
218
|
if (s.owner.streams.get(s.ownerId) !== s)
|
|
@@ -240,7 +241,19 @@ export class RelayRouter {
|
|
|
240
241
|
provider.streams.delete(s.providerId);
|
|
241
242
|
continue;
|
|
242
243
|
}
|
|
243
|
-
|
|
244
|
+
const deadline = Math.min(Date.now() + this.limits.connectTimeoutMs, s.expires);
|
|
245
|
+
const expire = () => {
|
|
246
|
+
// Timers can fire just before their absolute deadline. Do not fail a
|
|
247
|
+
// provider early and start a backup beyond the original route budget.
|
|
248
|
+
const remaining = deadline - Date.now();
|
|
249
|
+
if (remaining > 0) {
|
|
250
|
+
s.deadline = setTimeout(expire, remaining);
|
|
251
|
+
s.deadline.unref?.();
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
this.providerFailed(s, 'Target timeout');
|
|
255
|
+
};
|
|
256
|
+
s.deadline = setTimeout(expire, Math.max(0, deadline - Date.now()));
|
|
244
257
|
s.deadline.unref?.();
|
|
245
258
|
return true;
|
|
246
259
|
}
|
|
@@ -264,10 +277,15 @@ export class RelayRouter {
|
|
|
264
277
|
return;
|
|
265
278
|
}
|
|
266
279
|
if (f.type === 'open') {
|
|
280
|
+
if (this.options.isLocalTarget?.(f.serviceId)) {
|
|
281
|
+
send(peer.ws, { type: 'close', streamId: f.streamId, reason: 'Cannot relay to the entry itself', code: 'RELAY_SELF_TARGET' }, this.limits);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
267
284
|
if (peer.streams.has(f.streamId))
|
|
268
285
|
throw new Error('Duplicate stream');
|
|
269
286
|
if (!this.canRoute(peer, f.serviceId) || !this.hasRoute(f.serviceId)) {
|
|
270
|
-
send(peer.ws, { type: 'close', streamId: f.streamId, reason: 'Route unavailable or denied'
|
|
287
|
+
send(peer.ws, { type: 'close', streamId: f.streamId, reason: 'Route unavailable or denied',
|
|
288
|
+
...(!this.canRoute(peer, f.serviceId) ? { code: 'RELAY_AUTHORIZATION_REQUIRED' } : {}) }, this.limits);
|
|
271
289
|
return;
|
|
272
290
|
}
|
|
273
291
|
if (peer.streams.size >= this.limits.maxStreams)
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
// Object identity is deliberately used instead of a remotely forgeable header.
|
|
2
|
+
const resources = new WeakMap();
|
|
3
|
+
export function encryptedRequestResources(request) { return resources.get(request); }
|
|
2
4
|
const verified = new WeakSet();
|
|
3
5
|
const subjects = new WeakMap();
|
|
4
6
|
const serviceAccess = new WeakSet();
|
|
5
|
-
export function markEncryptedRequest(request, subjectId, fullService = false) {
|
|
7
|
+
export function markEncryptedRequest(request, subjectId, fullService = false, shared) {
|
|
6
8
|
verified.add(request);
|
|
9
|
+
if (shared)
|
|
10
|
+
resources.set(request, shared);
|
|
7
11
|
if (subjectId)
|
|
8
12
|
subjects.set(request, subjectId);
|
|
9
13
|
if (fullService)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { realpathSync, statSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { canonicalApiPath } from './accessPolicy.js';
|
|
4
|
+
export function computerResource(resources, host, protocol = 'vnc', port = protocol === 'rdp' ? 3389 : 5900) {
|
|
5
|
+
const matches = resources.computers?.filter(c => c.host.toLowerCase() === host.toLowerCase() && c.protocol === protocol && c.port === port);
|
|
6
|
+
return matches?.find(c => c.access === 'control') ?? matches?.[0];
|
|
7
|
+
}
|
|
8
|
+
/** Check both the lexical and real path. Symlinks cannot widen a shared root. */
|
|
9
|
+
export function directoryResource(resources, requested, write = false) {
|
|
10
|
+
if (!path.isAbsolute(requested) || requested.includes('\0'))
|
|
11
|
+
return false;
|
|
12
|
+
try {
|
|
13
|
+
const lexical = path.resolve(requested), real = realpathSync(lexical);
|
|
14
|
+
const inside = (root, file) => { const rel = path.relative(root, file); return rel === '' || (!rel.startsWith(`..${path.sep}`) && rel !== '..' && !path.isAbsolute(rel)); };
|
|
15
|
+
return resources.directories?.some(root => (!write || root.access === 'write') && inside(path.resolve(root.path), lexical) && inside(realpathSync(root.path), real)) === true;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function canonicalShareDirectories(resources) {
|
|
22
|
+
return { ...resources, ...(resources.directories ? { directories: resources.directories.map(root => {
|
|
23
|
+
const canonical = realpathSync(root.path);
|
|
24
|
+
if (!statSync(canonical).isDirectory())
|
|
25
|
+
throw new Error('SHARE_DIRECTORY_UNAVAILABLE');
|
|
26
|
+
return { ...root, path: canonical };
|
|
27
|
+
}) } : {}) };
|
|
28
|
+
}
|
|
29
|
+
export function resourceHttpAllowed(resources, method, requestPath) {
|
|
30
|
+
const pathname = canonicalApiPath(requestPath);
|
|
31
|
+
if (!pathname)
|
|
32
|
+
return false;
|
|
33
|
+
if (method === 'GET' && pathname === '/api/android/devices')
|
|
34
|
+
return !!resources.android?.length;
|
|
35
|
+
if (resources.android?.length && ((['GET', 'POST'].includes(method) && pathname === '/api/android/recordings') || (method === 'POST' && /^\/api\/android\/recordings\/[a-f0-9-]{36}\/(?:stop|save)$/.test(pathname)) || (method === 'DELETE' && /^\/api\/android\/recordings\/[a-f0-9-]{36}$/.test(pathname))))
|
|
36
|
+
return true;
|
|
37
|
+
if (method === 'GET' && pathname === '/api/computer/status')
|
|
38
|
+
return !!resources.computers?.length;
|
|
39
|
+
const url = new URL(requestPath, 'http://inner');
|
|
40
|
+
const op = pathname.slice('/api/terminal/fs/'.length);
|
|
41
|
+
if (['GET', 'HEAD'].includes(method) && op.startsWith('preview/')) {
|
|
42
|
+
const parts = op.slice(8).split('/');
|
|
43
|
+
if (/^[0-9a-f]{32}$/.test(parts[0]))
|
|
44
|
+
parts.shift();
|
|
45
|
+
return directoryResource(resources, '/' + parts.join('/'));
|
|
46
|
+
}
|
|
47
|
+
if (!pathname.startsWith('/api/terminal/fs/'))
|
|
48
|
+
return false;
|
|
49
|
+
// Unregistered Git, shell, native-open and recursive APIs remain unavailable.
|
|
50
|
+
const read = ['GET', 'HEAD'].includes(method) && ['list', 'read', 'blob', 'video', 'download', 'eda-preview', 'eda-inspect'].includes(op);
|
|
51
|
+
const write = (method === 'POST' && op === 'upload') || (method === 'PUT' && op === 'write') || (method === 'DELETE' && op === 'file');
|
|
52
|
+
if (!read && !write)
|
|
53
|
+
return false;
|
|
54
|
+
const key = op === 'upload' ? 'dir' : 'path';
|
|
55
|
+
if (url.searchParams.getAll(key).length !== 1 || !directoryResource(resources, url.searchParams.get(key) ?? '', write))
|
|
56
|
+
return false;
|
|
57
|
+
// Listing can run git check-ignore; its repository is another filesystem input.
|
|
58
|
+
for (const extra of ['gitIgnoreRoot', 'cwd', 'root', 'repoRoot']) {
|
|
59
|
+
if (url.searchParams.has(extra) && (url.searchParams.getAll(extra).length !== 1 || !directoryResource(resources, url.searchParams.get(extra) ?? '')))
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { SessionSharingPresence } from './sessionSharing.js';
|
|
2
|
+
import { collaborationSessionDisplayName } from '../utils/sessionDisplayName.js';
|
|
3
|
+
import { mergeShareResources } from '../shared/shareResources.js';
|
|
4
|
+
import { canonicalShareDirectories, computerResource, resourceHttpAllowed } from './resourceAccess.js';
|
|
1
5
|
import { CollaborationRpc } from '../agent/collaborationPeerTransport.js';
|
|
2
6
|
import { PasswordBootstrapServer } from './passwordBootstrap.js';
|
|
3
7
|
import { DeviceProfiles } from './deviceProfiles.js';
|
|
@@ -92,6 +96,7 @@ export async function createFederationRuntime(app, directory, handlers, options
|
|
|
92
96
|
const passwordStarts = [];
|
|
93
97
|
const passwordAttempts = new Map();
|
|
94
98
|
const store = new AuthorizationStore({ serviceId, filePath: join(directory, 'grants.json'), passwordCredential: getPasswordCredentialFingerprint });
|
|
99
|
+
const sharingPresence = new SessionSharingPresence();
|
|
95
100
|
const deviceProfiles = new DeviceProfiles(join(directory, 'device-profiles.json'));
|
|
96
101
|
const deviceNames = new DeviceNames(join(directory, 'device-names.json'));
|
|
97
102
|
const invitations = new InvitationStore({ serviceId, filePath: join(directory, 'invitations.json') });
|
|
@@ -113,7 +118,9 @@ export async function createFederationRuntime(app, directory, handlers, options
|
|
|
113
118
|
delete request.headers['x-termdock-device'];
|
|
114
119
|
const fullService = request.headers['x-termdock-full-service'] === '1';
|
|
115
120
|
delete request.headers['x-termdock-full-service'];
|
|
116
|
-
|
|
121
|
+
const resourcesHeader = request.headers['x-termdock-resources'];
|
|
122
|
+
delete request.headers['x-termdock-resources'];
|
|
123
|
+
markEncryptedRequest(request, typeof subject === 'string' ? subject : undefined, fullService, typeof resourcesHeader === 'string' ? JSON.parse(resourcesHeader) : undefined);
|
|
117
124
|
app(request, response);
|
|
118
125
|
});
|
|
119
126
|
// The outer encrypted consumer can apply backpressure long after a local
|
|
@@ -163,10 +170,14 @@ export async function createFederationRuntime(app, directory, handlers, options
|
|
|
163
170
|
const stable = stableSessionId(sessionId);
|
|
164
171
|
return stable !== undefined && store.authorize({ subjectId, serviceId, action, sessionId: stable }).allowed;
|
|
165
172
|
};
|
|
173
|
+
const resources = (subjectId) => mergeShareResources(store.listEffective({ subjectId, serviceId }).map(g => g.resources));
|
|
174
|
+
const androidAllowed = (subjectId, serial, control = false) => full(subjectId) || resources(subjectId).android?.some(d => d.serial === serial && (!control || d.access === 'control')) === true;
|
|
175
|
+
const computerAllowed = (subjectId, target) => full(subjectId) || !!computerResource(resources(subjectId), target.host, target.protocol, target.port);
|
|
166
176
|
const full = (subjectId) => open() || store.hasFullServiceAccess({ subjectId, serviceId });
|
|
167
177
|
let channel;
|
|
168
178
|
const operations = new Map();
|
|
169
179
|
const sockets = new Map();
|
|
180
|
+
const presenceKey = randomUUID();
|
|
170
181
|
let revokeTimer;
|
|
171
182
|
try {
|
|
172
183
|
const secured = await secureConnection({ identity, duplex, initiator: false, signal: AbortSignal.timeout(15000) });
|
|
@@ -180,23 +191,26 @@ export async function createFederationRuntime(app, directory, handlers, options
|
|
|
180
191
|
if (typeof head.method !== 'string' || typeof head.path !== 'string' || !isBusinessApiPath(head.method, head.path))
|
|
181
192
|
throw new Error('API_NOT_ALLOWED');
|
|
182
193
|
const policy = resolveAccessRequest(head.method, head.path);
|
|
183
|
-
if (!full(subjectId)) {
|
|
194
|
+
if (!full(subjectId) && !resourceHttpAllowed(resources(subjectId), head.method, head.path)) {
|
|
184
195
|
if (!policy)
|
|
185
196
|
throw new Error('API_NOT_ALLOWED');
|
|
186
197
|
check(policy.action, policy.sessionId);
|
|
187
198
|
}
|
|
188
199
|
};
|
|
189
200
|
async function runHttp(id, operation) {
|
|
201
|
+
const scopeStamp = JSON.stringify([full(subjectId), resources(subjectId)]);
|
|
190
202
|
const sendHttp = (packet) => {
|
|
191
203
|
if (operations.get(id) !== operation || operation.abort.signal.aborted)
|
|
192
204
|
throw new Error('CANCELLED');
|
|
205
|
+
if (scopeStamp !== JSON.stringify([full(subjectId), resources(subjectId)]))
|
|
206
|
+
throw new Error('AUTHORIZATION_CHANGED');
|
|
193
207
|
httpAllowed(operation.head);
|
|
194
208
|
send(packet);
|
|
195
209
|
};
|
|
196
210
|
try {
|
|
197
211
|
httpAllowed(operation.head);
|
|
198
212
|
const headers = new Headers({ 'x-termdock-inner': internalToken, 'x-termdock-device': subjectId,
|
|
199
|
-
'x-termdock-full-service': full(subjectId) ? '1' : '0' });
|
|
213
|
+
'x-termdock-full-service': full(subjectId) ? '1' : '0', 'x-termdock-resources': JSON.stringify(resources(subjectId)) });
|
|
200
214
|
const supplied = operation.head.headers;
|
|
201
215
|
if (supplied && typeof supplied === 'object')
|
|
202
216
|
for (const [name, value] of Object.entries(supplied)) {
|
|
@@ -259,9 +273,9 @@ export async function createFederationRuntime(app, directory, handlers, options
|
|
|
259
273
|
}
|
|
260
274
|
}
|
|
261
275
|
revokeTimer = setInterval(() => {
|
|
262
|
-
for (const { socket: logical, sessionId, android, computer } of sockets.values()) {
|
|
276
|
+
for (const { socket: logical, sessionId, android, computer, computerTarget, computerControl } of sockets.values()) {
|
|
263
277
|
if (android || computer) {
|
|
264
|
-
if (!full(subjectId))
|
|
278
|
+
if (android ? !androidAllowed(subjectId, android) : !computerTarget || !computerAllowed(subjectId, computerTarget) || (computerControl && !full(subjectId) && !resources(subjectId).computers?.some(c => c.host.toLowerCase() === computerTarget.host.toLowerCase() && c.protocol === computerTarget.protocol && c.port === computerTarget.port && c.access === 'control')))
|
|
265
279
|
logical.close(4003, 'Authorization revoked');
|
|
266
280
|
continue;
|
|
267
281
|
}
|
|
@@ -389,7 +403,7 @@ export async function createFederationRuntime(app, directory, handlers, options
|
|
|
389
403
|
}
|
|
390
404
|
else if (packet.type === 'permissions') {
|
|
391
405
|
const grants = store.listEffective({ subjectId, serviceId });
|
|
392
|
-
send({ type: 'result', id: packet.id, subjectId, serviceId, fullService: full(subjectId), grants, httpWindow: MAX_TRANSFER_WINDOW, binaryChunks: true, webTransport: options.webTransport?.() });
|
|
406
|
+
send({ type: 'result', id: packet.id, subjectId, serviceId, fullService: full(subjectId), grants, httpWindow: MAX_TRANSFER_WINDOW, binaryChunks: true, relayAuthorizationErrors: true, relaySelfTargetGuard: true, webTransport: options.webTransport?.() });
|
|
393
407
|
}
|
|
394
408
|
else if (packet.type === 'session-list') {
|
|
395
409
|
const inventory = await sessionInventory();
|
|
@@ -400,17 +414,34 @@ export async function createFederationRuntime(app, directory, handlers, options
|
|
|
400
414
|
return [];
|
|
401
415
|
seen.add(sessionId);
|
|
402
416
|
return [{ sessionId, ...(typeof session.sessionId === 'string' ? { sourceSessionId: session.sessionId } : {}),
|
|
403
|
-
name: typeof session.name === 'string' ? session.name : sessionId, live: session.live === true,
|
|
417
|
+
name: collaborationSessionDisplayName({ ...session, name: typeof session.name === 'string' ? session.name : sessionId }), live: session.live === true,
|
|
404
418
|
canWrite: allowed(subjectId, 'session.input', sessionId), canResize: allowed(subjectId, 'session.resize', sessionId) }];
|
|
405
419
|
});
|
|
406
420
|
send({ type: 'result', id: packet.id, items });
|
|
407
421
|
}
|
|
422
|
+
else if (packet.type === 'session-sharing-status') {
|
|
423
|
+
check('authorization.manage');
|
|
424
|
+
const normalize = (id) => options.resolveSessionId?.(id) || id;
|
|
425
|
+
send({ type: 'result', id: packet.id, items: sharingPresence.summary(store.listActive(), invitations.listPending(), normalize) });
|
|
426
|
+
}
|
|
427
|
+
else if (packet.type === 'session-sharing-stop') {
|
|
428
|
+
check('authorization.manage');
|
|
429
|
+
if (typeof packet.sessionId !== 'string' || !packet.sessionId)
|
|
430
|
+
throw new Error('INVALID_SESSION');
|
|
431
|
+
const normalize = (id) => options.resolveSessionId?.(id) || id;
|
|
432
|
+
const sessionId = normalize(packet.sessionId);
|
|
433
|
+
const revoked = store.revokeSession(sessionId, normalize);
|
|
434
|
+
invitations.revokeSession(sessionId, normalize);
|
|
435
|
+
send({ type: 'result', id: packet.id, revoked });
|
|
436
|
+
}
|
|
408
437
|
else if (packet.type === 'self-relay-ticket') {
|
|
409
438
|
if (!options.issueSelfRelayTicket)
|
|
410
439
|
throw new Error('SELF_RELAY_UNAVAILABLE');
|
|
411
440
|
send({ type: 'result', id: packet.id, serviceId: subjectId, ...options.issueSelfRelayTicket(subjectId) });
|
|
412
441
|
}
|
|
413
442
|
else if (packet.type === 'route-invite-create') {
|
|
443
|
+
if (packet.serviceId === serviceId)
|
|
444
|
+
throw new Error('RELAY_SELF_TARGET');
|
|
414
445
|
check('authorization.manage');
|
|
415
446
|
if (!options.createRouteInvitation || typeof packet.serviceId !== 'string' || !packet.serviceId || packet.serviceId.length > 512)
|
|
416
447
|
throw new Error('ROUTE_NOT_AVAILABLE');
|
|
@@ -433,6 +464,8 @@ export async function createFederationRuntime(app, directory, handlers, options
|
|
|
433
464
|
send({ type: 'result', id: packet.id, canManage, grants: canManage ? all : all.filter(grant => grant.subjectId === subjectId) });
|
|
434
465
|
}
|
|
435
466
|
else if (packet.type === 'route-grant') {
|
|
467
|
+
if (packet.serviceId === serviceId)
|
|
468
|
+
throw new Error('RELAY_SELF_TARGET');
|
|
436
469
|
check('authorization.manage');
|
|
437
470
|
if (!options.grantRouteAccess || typeof packet.serviceId !== 'string' || typeof packet.subjectId !== 'string')
|
|
438
471
|
throw new Error('ROUTE_NOT_AVAILABLE');
|
|
@@ -447,6 +480,8 @@ export async function createFederationRuntime(app, directory, handlers, options
|
|
|
447
480
|
send({ type: 'result', id: packet.id, revoked: options.revokeRouteAccess(packet.grantId) });
|
|
448
481
|
}
|
|
449
482
|
else if (packet.type === 'route-ticket') {
|
|
483
|
+
if (packet.serviceId === serviceId)
|
|
484
|
+
throw new Error('RELAY_SELF_TARGET');
|
|
450
485
|
if (typeof packet.serviceId !== 'string' || !options.hasRouteGrant?.(subjectId, packet.serviceId))
|
|
451
486
|
throw new Error('AUTHORIZATION_DENIED');
|
|
452
487
|
if (!options.issueRouteTicket || typeof packet.serviceId !== 'string' || !packet.serviceId || packet.serviceId.length > 512)
|
|
@@ -455,10 +490,12 @@ export async function createFederationRuntime(app, directory, handlers, options
|
|
|
455
490
|
}
|
|
456
491
|
else if (packet.type === 'invite-create') {
|
|
457
492
|
check('authorization.manage');
|
|
458
|
-
const invitation = { scope: packet.scope, actions: packet.actions,
|
|
493
|
+
const invitation = { scope: packet.scope, actions: packet.actions, ...(packet.resources === undefined ? {} : { resources: packet.resources }),
|
|
459
494
|
...(packet.label === undefined ? {} : { label: packet.label }),
|
|
460
495
|
...(packet.expiresAt === undefined ? {} : { expiresAt: packet.expiresAt }) };
|
|
461
496
|
validateInvitation(invitation);
|
|
497
|
+
if (invitation.resources)
|
|
498
|
+
invitation.resources = canonicalShareDirectories(invitation.resources);
|
|
462
499
|
if (invitation.scope.kind === 'sessions') {
|
|
463
500
|
// Bind to the stable client session id, never the transient backend
|
|
464
501
|
// PTY id: backends are replaced on restart, tmux recovery or
|
|
@@ -518,6 +555,9 @@ export async function createFederationRuntime(app, directory, handlers, options
|
|
|
518
555
|
if ((context.singleOperation ? operations.size + sockets.size >= 1 : operations.size >= MAX_SERVER_HTTP_REQUESTS) || operations.has(packet.id) || sockets.has(packet.id))
|
|
519
556
|
throw new Error('STREAM_LIMIT');
|
|
520
557
|
httpAllowed(packet);
|
|
558
|
+
const requestedSession = resolveAccessRequest(String(packet.method), String(packet.path))?.sessionId;
|
|
559
|
+
if (requestedSession && !full(subjectId))
|
|
560
|
+
sharingPresence.visit(presenceKey, subjectId, stableSessionId(requestedSession) || requestedSession);
|
|
521
561
|
const upload = !['GET', 'HEAD'].includes(String(packet.method));
|
|
522
562
|
if (upload && uploadSlots >= 8)
|
|
523
563
|
throw new Error('UPLOAD_CONCURRENCY_LIMIT');
|
|
@@ -581,26 +621,34 @@ export async function createFederationRuntime(app, directory, handlers, options
|
|
|
581
621
|
const match = /^\/api\/terminal\/([^/%]+)\/ws$/.exec(url.pathname);
|
|
582
622
|
const androidMatch = /^\/api\/android\/([^/%]+)\/ws$/.exec(url.pathname);
|
|
583
623
|
const computer = url.pathname === '/api/computer/ws';
|
|
584
|
-
|
|
624
|
+
const computerTarget = { host: url.searchParams.get('host') ?? '', protocol: url.searchParams.get('protocol') ?? 'vnc', port: Number(url.searchParams.get('port') ?? (url.searchParams.get('protocol') === 'rdp' ? '3389' : '5900')) };
|
|
625
|
+
const canView = () => androidMatch ? androidAllowed(subjectId, androidMatch[1]) : computer ? computerAllowed(subjectId, computerTarget) : allowed(subjectId, match ? 'session.view' : 'service.view', match?.[1]);
|
|
626
|
+
if (match) {
|
|
585
627
|
check('session.view', match[1]);
|
|
628
|
+
if (!full(subjectId))
|
|
629
|
+
sharingPresence.visit(presenceKey, subjectId, stableSessionId(match[1]) || match[1]);
|
|
630
|
+
}
|
|
586
631
|
else if (url.pathname === '/api/control/ws')
|
|
587
632
|
check('service.view');
|
|
588
633
|
else if (androidMatch || computer) {
|
|
589
|
-
if (!
|
|
634
|
+
if (!canView())
|
|
590
635
|
throw new Error('API_NOT_ALLOWED');
|
|
591
636
|
}
|
|
592
637
|
else
|
|
593
638
|
throw new Error('API_NOT_ALLOWED');
|
|
594
|
-
const logical = new LogicalSocket(channel, packet.id,
|
|
595
|
-
sockets.set(packet.id, { socket: logical, sessionId: match?.[1], android:
|
|
596
|
-
computerProtocol: computer ? url.searchParams.get('protocol') ?? 'vnc' : undefined });
|
|
597
|
-
|
|
639
|
+
const logical = new LogicalSocket(channel, packet.id, canView, match ? 1024 * 1024 : undefined, !!match && packet.compression === 'gzip');
|
|
640
|
+
sockets.set(packet.id, { socket: logical, sessionId: match?.[1], android: androidMatch?.[1], computer, computerTarget: computer ? computerTarget : undefined,
|
|
641
|
+
computerControl: computer && (full(subjectId) || resources(subjectId).computers?.some(c => c.host.toLowerCase() === computerTarget.host.toLowerCase() && c.protocol === computerTarget.protocol && c.port === computerTarget.port && c.access === 'control')), computerProtocol: computer ? url.searchParams.get('protocol') ?? 'vnc' : undefined });
|
|
642
|
+
const shareStreamKey = `${presenceKey}:${packet.id}`;
|
|
643
|
+
if (match && !full(subjectId))
|
|
644
|
+
sharingPresence.openStream(shareStreamKey, presenceKey, subjectId, stableSessionId(match[1]) || match[1]);
|
|
645
|
+
logical.once('close', () => { sockets.delete(packet.id); sharingPresence.closeStream(shareStreamKey); });
|
|
598
646
|
send({ type: 'ws-ready', id: packet.id });
|
|
599
647
|
if (match)
|
|
600
648
|
handlers.terminal(logical, match[1], randomUUID(), {
|
|
601
649
|
pushClientId: `device:${subjectId}`, sinceSeq: Math.max(0, Number(url.searchParams.get('since')) || 0), streamEpoch: url.searchParams.get('epoch') ?? undefined,
|
|
602
650
|
flowControl: url.searchParams.get('flow') === '2', outputActive: url.searchParams.get('active') !== '0',
|
|
603
|
-
independentTmux:
|
|
651
|
+
independentTmux: allowed(subjectId, 'session.resize', match[1]) && url.searchParams.get('transport') === 'tmux-client',
|
|
604
652
|
}, allowed(subjectId, 'session.resize', match[1]) ? readTerminalHandshakeDimensions(url.searchParams) : undefined);
|
|
605
653
|
else if (androidMatch) {
|
|
606
654
|
if (!handlers.android)
|
|
@@ -617,7 +665,7 @@ export async function createFederationRuntime(app, directory, handlers, options
|
|
|
617
665
|
continue;
|
|
618
666
|
}
|
|
619
667
|
handlers.computer(logical, url.searchParams.get('host') ?? '', {
|
|
620
|
-
protocol:
|
|
668
|
+
protocol: computerTarget.protocol, port: String(computerTarget.port), readOnly: !full(subjectId) && computerResource(resources(subjectId), computerTarget.host, computerTarget.protocol, computerTarget.port)?.access !== 'control',
|
|
621
669
|
});
|
|
622
670
|
}
|
|
623
671
|
else
|
|
@@ -629,14 +677,14 @@ export async function createFederationRuntime(app, directory, handlers, options
|
|
|
629
677
|
throw new Error('INVALID_STREAM');
|
|
630
678
|
const data = JSON.parse(packet.data);
|
|
631
679
|
if (item.computer) {
|
|
632
|
-
if (!full(subjectId))
|
|
680
|
+
if (!item.computerTarget || !computerAllowed(subjectId, item.computerTarget) || (item.computerControl && !full(subjectId) && !resources(subjectId).computers?.some(c => c.host.toLowerCase() === item.computerTarget.host.toLowerCase() && c.protocol === item.computerTarget.protocol && c.port === item.computerTarget.port && c.access === 'control')))
|
|
633
681
|
throw new Error('AUTHORIZATION_DENIED');
|
|
634
682
|
const types = item.computerProtocol === 'rdp' ? ['start', 'instruction', 'ack'] : ['data', 'ack'];
|
|
635
683
|
if (!types.includes(data?.type))
|
|
636
684
|
throw new Error('ACTION_NOT_ALLOWED');
|
|
637
685
|
}
|
|
638
686
|
else if (item.android) {
|
|
639
|
-
if (!
|
|
687
|
+
if (!androidAllowed(subjectId, item.android, data?.type === 'control' && data?.data !== 'EQ=='))
|
|
640
688
|
throw new Error('AUTHORIZATION_DENIED');
|
|
641
689
|
if (!['control', 'ack', 'ping', 'bitrate'].includes(data?.type))
|
|
642
690
|
throw new Error('ACTION_NOT_ALLOWED');
|
|
@@ -672,6 +720,7 @@ export async function createFederationRuntime(app, directory, handlers, options
|
|
|
672
720
|
duplex.close?.(new Error('Secure channel closed'));
|
|
673
721
|
}
|
|
674
722
|
finally {
|
|
723
|
+
sharingPresence.close(presenceKey);
|
|
675
724
|
unregisterCollaboration?.();
|
|
676
725
|
collaborationRpc?.close();
|
|
677
726
|
clearInterval(revokeTimer);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export class SessionSharingPresence {
|
|
2
|
+
connections = new Map();
|
|
3
|
+
visit(connection, device, session) {
|
|
4
|
+
const current = this.connections.get(connection) || { device, sessions: new Map() };
|
|
5
|
+
current.sessions.set(session, Date.now());
|
|
6
|
+
this.connections.set(connection, current);
|
|
7
|
+
}
|
|
8
|
+
streams = new Map();
|
|
9
|
+
openStream(key, connection, device, session) { this.streams.set(key, { connection, device, session }); }
|
|
10
|
+
closeStream(key) { this.streams.delete(key); }
|
|
11
|
+
close(connection) { this.connections.delete(connection); for (const [key, stream] of this.streams)
|
|
12
|
+
if (stream.connection === connection)
|
|
13
|
+
this.streams.delete(key); }
|
|
14
|
+
summary(grants, invitations, normalize) {
|
|
15
|
+
const result = new Map();
|
|
16
|
+
const record = (id) => { const key = normalize(id); let item = result.get(key); if (!item) {
|
|
17
|
+
item = { sessionId: key, devices: new Set(), invitations: 0 };
|
|
18
|
+
result.set(key, item);
|
|
19
|
+
} return item; };
|
|
20
|
+
for (const grant of grants)
|
|
21
|
+
if (grant.scope.kind === 'sessions')
|
|
22
|
+
for (const id of grant.scope.sessionIds)
|
|
23
|
+
record(id).devices.add(grant.subjectId);
|
|
24
|
+
for (const invite of invitations)
|
|
25
|
+
if (invite.scope.kind === 'sessions')
|
|
26
|
+
for (const id of invite.scope.sessionIds)
|
|
27
|
+
record(id).invitations++;
|
|
28
|
+
return [...result.values()].map(item => {
|
|
29
|
+
const online = new Set();
|
|
30
|
+
for (const connection of this.connections.values())
|
|
31
|
+
if (Date.now() - (connection.sessions.get(item.sessionId) || 0) < 15_000 && item.devices.has(connection.device))
|
|
32
|
+
online.add(connection.device);
|
|
33
|
+
for (const stream of this.streams.values())
|
|
34
|
+
if (stream.session === item.sessionId && item.devices.has(stream.device))
|
|
35
|
+
online.add(stream.device);
|
|
36
|
+
return { sessionId: item.sessionId, devices: item.devices.size, connectedDevices: online.size, invitations: item.invitations };
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -13,8 +13,8 @@ export function socketDuplex(socket) {
|
|
|
13
13
|
close(new Error('Encrypted transport rejected frame'));
|
|
14
14
|
}
|
|
15
15
|
});
|
|
16
|
-
socket.addEventListener('close', () => input.end());
|
|
17
|
-
socket.addEventListener('error', () => input.end(new Error('Encrypted transport disconnected')));
|
|
16
|
+
socket.addEventListener('close', () => input.end(socket.transportFailure));
|
|
17
|
+
socket.addEventListener('error', () => input.end(socket.transportFailure || new Error('Encrypted transport disconnected')));
|
|
18
18
|
return {
|
|
19
19
|
source: input, close,
|
|
20
20
|
async sink(source) {
|
|
@@ -28,7 +28,7 @@ export function socketDuplex(socket) {
|
|
|
28
28
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
29
29
|
}
|
|
30
30
|
if (socket.readyState !== 1)
|
|
31
|
-
throw new Error('Encrypted transport closed');
|
|
31
|
+
throw socket.transportFailure || new Error('Encrypted transport closed');
|
|
32
32
|
socket.send(chunk.subarray(offset, offset + 64 * 1024));
|
|
33
33
|
}
|
|
34
34
|
}
|