termdock 1.4.173 → 1.4.175
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 +204 -7
- package/dist/client/assets/App-BXYZyI_k.js +150 -0
- package/dist/client/assets/App-BXYZyI_k.js.br +0 -0
- package/dist/client/assets/App-BXYZyI_k.js.gz +0 -0
- package/dist/client/assets/ChangeWalkthroughPanel-DeVtCf73.js +2 -0
- package/dist/client/assets/ChangeWalkthroughPanel-DeVtCf73.js.br +0 -0
- package/dist/client/assets/ChangeWalkthroughPanel-DeVtCf73.js.gz +0 -0
- package/dist/client/assets/DagPlayground-ZIqLZoMo.js +1 -0
- package/dist/client/assets/DagPlayground-ZIqLZoMo.js.br +0 -0
- package/dist/client/assets/DagPlayground-ZIqLZoMo.js.gz +0 -0
- package/dist/client/assets/{DiffLab-xuS6rtpv.js → DiffLab-CSMf8So2.js} +2 -2
- package/dist/client/assets/DiffLab-CSMf8So2.js.br +0 -0
- package/dist/client/assets/DiffLab-CSMf8So2.js.gz +0 -0
- package/dist/client/assets/DiffReview-CE5lW1zQ.js +1 -0
- package/dist/client/assets/DiffReview-CE5lW1zQ.js.br +0 -0
- package/dist/client/assets/DiffReview-CE5lW1zQ.js.gz +0 -0
- package/dist/client/assets/DiffReviewLab-CpUW75fZ.js +2 -0
- package/dist/client/assets/DiffReviewLab-CpUW75fZ.js.br +0 -0
- package/dist/client/assets/DiffReviewLab-CpUW75fZ.js.gz +0 -0
- package/dist/client/assets/{DiffViewer-BQGXUr8t.js → DiffViewer-BomvZ4kq.js} +5 -5
- package/dist/client/assets/DiffViewer-BomvZ4kq.js.br +0 -0
- package/dist/client/assets/DiffViewer-BomvZ4kq.js.gz +0 -0
- package/dist/client/assets/{ModelPreview-BfZzS1_d.js → ModelPreview-BWdZFyhh.js} +3 -3
- package/dist/client/assets/ModelPreview-BWdZFyhh.js.br +0 -0
- package/dist/client/assets/ModelPreview-BWdZFyhh.js.gz +0 -0
- package/dist/client/assets/RightSidebar-CYVwFbOc.js +79 -0
- package/dist/client/assets/RightSidebar-CYVwFbOc.js.br +0 -0
- package/dist/client/assets/RightSidebar-CYVwFbOc.js.gz +0 -0
- package/dist/client/assets/{abnfDiagram-VCTEODGH-Dn_lmPcC.js → abnfDiagram-VCTEODGH-BIPsLStE.js} +1 -1
- package/dist/client/assets/abnfDiagram-VCTEODGH-BIPsLStE.js.br +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-BIPsLStE.js.gz +0 -0
- package/dist/client/assets/app-D6KzR8B3.js +34 -0
- package/dist/client/assets/app-D6KzR8B3.js.br +0 -0
- package/dist/client/assets/app-D6KzR8B3.js.gz +0 -0
- package/dist/client/assets/{arc-DUybtKGZ.js → arc-DzeT6hBj.js} +1 -1
- package/dist/client/assets/arc-DzeT6hBj.js.br +0 -0
- package/dist/client/assets/arc-DzeT6hBj.js.gz +0 -0
- package/dist/client/assets/{architectureDiagram-5GKGNRK7-CLEmOHBp.js → architectureDiagram-5GKGNRK7-B1qsxd_5.js} +1 -1
- package/dist/client/assets/architectureDiagram-5GKGNRK7-B1qsxd_5.js.br +0 -0
- package/dist/client/assets/architectureDiagram-5GKGNRK7-B1qsxd_5.js.gz +0 -0
- package/dist/client/assets/{blockDiagram-I7D4REHJ-O_XM5b9_.js → blockDiagram-I7D4REHJ-NLsa29Jp.js} +1 -1
- package/dist/client/assets/blockDiagram-I7D4REHJ-NLsa29Jp.js.br +0 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-NLsa29Jp.js.gz +0 -0
- package/dist/client/assets/{c4Diagram-7LVT6UL2-DlUEslcV.js → c4Diagram-7LVT6UL2-DBAUvou3.js} +1 -1
- package/dist/client/assets/c4Diagram-7LVT6UL2-DBAUvou3.js.br +0 -0
- package/dist/client/assets/c4Diagram-7LVT6UL2-DBAUvou3.js.gz +0 -0
- package/dist/client/assets/channel-BhwBAWhq.js +1 -0
- package/dist/client/assets/{chunk-2Q5K7J3B-DCrP6W79.js → chunk-2Q5K7J3B-BsVAXGLa.js} +1 -1
- package/dist/client/assets/{chunk-5VM5RSS4-Zm6_pYQv.js → chunk-5VM5RSS4-BKtILsst.js} +1 -1
- package/dist/client/assets/{chunk-F27PBJKO-DkyD-X9z.js → chunk-F27PBJKO-Dn9zcQZG.js} +1 -1
- package/dist/client/assets/chunk-F27PBJKO-Dn9zcQZG.js.br +0 -0
- package/dist/client/assets/chunk-F27PBJKO-Dn9zcQZG.js.gz +0 -0
- package/dist/client/assets/{chunk-IMKFNOWR-UvWxXAkh.js → chunk-IMKFNOWR-MVtVQeTr.js} +1 -1
- package/dist/client/assets/chunk-IMKFNOWR-MVtVQeTr.js.br +0 -0
- package/dist/client/assets/chunk-IMKFNOWR-MVtVQeTr.js.gz +0 -0
- package/dist/client/assets/{chunk-JWPE2WC7-BjeBAFYM.js → chunk-JWPE2WC7-DliIIMe7.js} +1 -1
- package/dist/client/assets/{chunk-POPQ4Y6H-CnqrQdZC.js → chunk-POPQ4Y6H-XBSus3IC.js} +1 -1
- package/dist/client/assets/{chunk-SVP7TREG-C4a1s0v4.js → chunk-SVP7TREG-wleSQcJR.js} +1 -1
- package/dist/client/assets/chunk-SVP7TREG-wleSQcJR.js.br +0 -0
- package/dist/client/assets/chunk-SVP7TREG-wleSQcJR.js.gz +0 -0
- package/dist/client/assets/{chunk-TICWLB2K-hnmjYAl_.js → chunk-TICWLB2K-Dq6LdswN.js} +1 -1
- package/dist/client/assets/chunk-TICWLB2K-Dq6LdswN.js.br +0 -0
- package/dist/client/assets/chunk-TICWLB2K-Dq6LdswN.js.gz +0 -0
- package/dist/client/assets/{chunk-XXDRQBXY-xR4LOz28.js → chunk-XXDRQBXY-xafyKlM-.js} +1 -1
- package/dist/client/assets/classDiagram-ZZMXUADV-slQB0ns2.js +1 -0
- package/dist/client/assets/classDiagram-v2-VYDZK3BY-slQB0ns2.js +1 -0
- package/dist/client/assets/{cose-bilkent-JH36ORCC-CqY84fFp.js → cose-bilkent-JH36ORCC-BR7Ohnm_.js} +1 -1
- package/dist/client/assets/cose-bilkent-JH36ORCC-BR7Ohnm_.js.br +0 -0
- package/dist/client/assets/cose-bilkent-JH36ORCC-BR7Ohnm_.js.gz +0 -0
- package/dist/client/assets/{cynefin-OW5HDTMX-Bd14zuaD.js → cynefin-OW5HDTMX-CmPjQgER.js} +1 -1
- package/dist/client/assets/cynefin-OW5HDTMX-CmPjQgER.js.br +0 -0
- package/dist/client/assets/cynefin-OW5HDTMX-CmPjQgER.js.gz +0 -0
- package/dist/client/assets/{cynefinDiagram-5FMLGOSQ-DSHcNXDs.js → cynefinDiagram-5FMLGOSQ-CFyR9RvM.js} +1 -1
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-CFyR9RvM.js.br +0 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-CFyR9RvM.js.gz +0 -0
- package/dist/client/assets/{dagre-GXQ25YYZ-CIGWQYeP.js → dagre-GXQ25YYZ-D_RVMRO1.js} +1 -1
- package/dist/client/assets/dagre-GXQ25YYZ-D_RVMRO1.js.br +0 -0
- package/dist/client/assets/dagre-GXQ25YYZ-D_RVMRO1.js.gz +0 -0
- package/dist/client/assets/{diagram-S7CK7UJ4-C9Fg7OLN.js → diagram-S7CK7UJ4-6fr-ZoPj.js} +1 -1
- package/dist/client/assets/diagram-S7CK7UJ4-6fr-ZoPj.js.br +0 -0
- package/dist/client/assets/diagram-S7CK7UJ4-6fr-ZoPj.js.gz +0 -0
- package/dist/client/assets/{diagram-UQ7AKVKN-DoRhl7Fd.js → diagram-UQ7AKVKN-CgwiUtz0.js} +1 -1
- package/dist/client/assets/diagram-UQ7AKVKN-CgwiUtz0.js.br +0 -0
- package/dist/client/assets/diagram-UQ7AKVKN-CgwiUtz0.js.gz +0 -0
- package/dist/client/assets/{diagram-VSXAHHWV-CI83g3iM.js → diagram-VSXAHHWV-CLxzTB9k.js} +1 -1
- package/dist/client/assets/diagram-VSXAHHWV-CLxzTB9k.js.br +0 -0
- package/dist/client/assets/diagram-VSXAHHWV-CLxzTB9k.js.gz +0 -0
- package/dist/client/assets/{diagram-VX7I27RA-Dp2kP4PB.js → diagram-VX7I27RA-DjxDAvdZ.js} +1 -1
- package/dist/client/assets/diagram-VX7I27RA-DjxDAvdZ.js.br +0 -0
- package/dist/client/assets/diagram-VX7I27RA-DjxDAvdZ.js.gz +0 -0
- package/dist/client/assets/{diagram-Z3DM3KII-KtLMCM-V.js → diagram-Z3DM3KII-CDPOjiz4.js} +1 -1
- package/dist/client/assets/diagram-Z3DM3KII-CDPOjiz4.js.br +0 -0
- package/dist/client/assets/diagram-Z3DM3KII-CDPOjiz4.js.gz +0 -0
- package/dist/client/assets/{ebnfDiagram-PWID7BFC-D_zseu1m.js → ebnfDiagram-PWID7BFC-BLbwyF1O.js} +1 -1
- package/dist/client/assets/ebnfDiagram-PWID7BFC-BLbwyF1O.js.br +0 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-BLbwyF1O.js.gz +0 -0
- package/dist/client/assets/{erDiagram-RLTQ6QDP-DaWUl_Kz.js → erDiagram-RLTQ6QDP-DbjtZnJx.js} +1 -1
- package/dist/client/assets/{erDiagram-RLTQ6QDP-DaWUl_Kz.js.br → erDiagram-RLTQ6QDP-DbjtZnJx.js.br} +0 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-DbjtZnJx.js.gz +0 -0
- package/dist/client/assets/{flowDiagram-HODETNUW-BMoJ46zJ.js → flowDiagram-HODETNUW-BirJInAN.js} +1 -1
- package/dist/client/assets/flowDiagram-HODETNUW-BirJInAN.js.br +0 -0
- package/dist/client/assets/flowDiagram-HODETNUW-BirJInAN.js.gz +0 -0
- package/dist/client/assets/{ganttDiagram-EL5Y4UJY-lV_iVmoW.js → ganttDiagram-EL5Y4UJY-d6Q9vSbv.js} +1 -1
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-d6Q9vSbv.js.br +0 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-d6Q9vSbv.js.gz +0 -0
- package/dist/client/assets/gestureArbiter-C2J0D0kV.css +1 -0
- package/dist/client/assets/gestureArbiter-C2J0D0kV.css.br +0 -0
- package/dist/client/assets/gestureArbiter-C2J0D0kV.css.gz +0 -0
- package/dist/client/assets/gestureArbiter-D1SNS_gK.js +1 -0
- package/dist/client/assets/gestureArbiter-D1SNS_gK.js.br +0 -0
- package/dist/client/assets/gestureArbiter-D1SNS_gK.js.gz +0 -0
- package/dist/client/assets/{gitGraphDiagram-WWUBYQGX-BVCT9Btd.js → gitGraphDiagram-WWUBYQGX-BWBoIeS6.js} +1 -1
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-BWBoIeS6.js.br +0 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-BWBoIeS6.js.gz +0 -0
- package/dist/client/assets/{index-CnKSUg74.css → index-DUI8KRf6.css} +1 -1
- package/dist/client/assets/index-DUI8KRf6.css.br +0 -0
- package/dist/client/assets/index-DUI8KRf6.css.gz +0 -0
- package/dist/client/assets/{infoDiagram-27XIBGKW-BVhXOx1S.js → infoDiagram-27XIBGKW-BH4AynkM.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-5VMMS53U-Cyzfuoys.js → ishikawaDiagram-5VMMS53U-BQw90AKR.js} +1 -1
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-BQw90AKR.js.br +0 -0
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-BQw90AKR.js.gz +0 -0
- package/dist/client/assets/{journeyDiagram-3NMN7TZE-CEIPDhQF.js → journeyDiagram-3NMN7TZE-DISkQocf.js} +1 -1
- package/dist/client/assets/journeyDiagram-3NMN7TZE-DISkQocf.js.br +0 -0
- package/dist/client/assets/journeyDiagram-3NMN7TZE-DISkQocf.js.gz +0 -0
- package/dist/client/assets/{kanban-definition-UXKFOSKX-C8OulEOF.js → kanban-definition-UXKFOSKX-Qg2LyEVy.js} +1 -1
- package/dist/client/assets/kanban-definition-UXKFOSKX-Qg2LyEVy.js.br +0 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-Qg2LyEVy.js.gz +0 -0
- package/dist/client/assets/{layout-YN93qRzb.js → layout-Cd7-4_Vr.js} +1 -1
- package/dist/client/assets/layout-Cd7-4_Vr.js.br +0 -0
- package/dist/client/assets/layout-Cd7-4_Vr.js.gz +0 -0
- package/dist/client/assets/{linear-D9T4dYh7.js → linear-DFTi2fTn.js} +1 -1
- package/dist/client/assets/linear-DFTi2fTn.js.br +0 -0
- package/dist/client/assets/linear-DFTi2fTn.js.gz +0 -0
- package/dist/client/assets/{mermaid.core-BdCvvCGV.js → mermaid.core-2v726lUU.js} +5 -5
- package/dist/client/assets/mermaid.core-2v726lUU.js.br +0 -0
- package/dist/client/assets/mermaid.core-2v726lUU.js.gz +0 -0
- package/dist/client/assets/{mindmap-definition-YA3MSWOX-CSpES3Jh.js → mindmap-definition-YA3MSWOX-_FpwteCy.js} +4 -4
- package/dist/client/assets/mindmap-definition-YA3MSWOX-_FpwteCy.js.br +0 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-_FpwteCy.js.gz +0 -0
- package/dist/client/assets/passwordBootstrap-D9Ffc7NY.js +2 -0
- package/dist/client/assets/passwordBootstrap-D9Ffc7NY.js.br +0 -0
- package/dist/client/assets/passwordBootstrap-D9Ffc7NY.js.gz +0 -0
- package/dist/client/assets/{pegDiagram-XKGWAZYB-6f4vAA1Q.js → pegDiagram-XKGWAZYB-DLFf-HmZ.js} +1 -1
- package/dist/client/assets/pegDiagram-XKGWAZYB-DLFf-HmZ.js.br +0 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-DLFf-HmZ.js.gz +0 -0
- package/dist/client/assets/{pieDiagram-E7YTZNPT-BC4Jd-I8.js → pieDiagram-E7YTZNPT-DqTnfXHG.js} +1 -1
- package/dist/client/assets/pieDiagram-E7YTZNPT-DqTnfXHG.js.br +0 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-DqTnfXHG.js.gz +0 -0
- package/dist/client/assets/{quadrantDiagram-AXDQQJYC-B0pnPaEN.js → quadrantDiagram-AXDQQJYC-B_oBH52Q.js} +1 -1
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-B_oBH52Q.js.br +0 -0
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-B_oBH52Q.js.gz +0 -0
- package/dist/client/assets/{railroadDiagram-O6MQD6OU-B-oOamZT.js → railroadDiagram-O6MQD6OU-HxFQ7Pce.js} +1 -1
- package/dist/client/assets/railroadDiagram-O6MQD6OU-HxFQ7Pce.js.br +0 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-HxFQ7Pce.js.gz +0 -0
- package/dist/client/assets/referenceLongPress-B3NGIjvv.js +1 -0
- package/dist/client/assets/referenceLongPress-B3NGIjvv.js.br +0 -0
- package/dist/client/assets/referenceLongPress-B3NGIjvv.js.gz +0 -0
- package/dist/client/assets/{requirementDiagram-BXWQKSXE-DBSyxt0k.js → requirementDiagram-BXWQKSXE-NZdfaZFk.js} +1 -1
- package/dist/client/assets/requirementDiagram-BXWQKSXE-NZdfaZFk.js.br +0 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-NZdfaZFk.js.gz +0 -0
- package/dist/client/assets/{sankeyDiagram-P5KCCOFB-CcohzEZE.js → sankeyDiagram-P5KCCOFB-BgCctv6X.js} +1 -1
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-BgCctv6X.js.br +0 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-BgCctv6X.js.gz +0 -0
- package/dist/client/assets/{sequenceDiagram-WJ2MYXX4-D7KS6rq_.js → sequenceDiagram-WJ2MYXX4-Br42Za1R.js} +1 -1
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-Br42Za1R.js.br +0 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-Br42Za1R.js.gz +0 -0
- package/dist/client/assets/{sizeCapture-INFHLROL-CaRXtn4g.js → sizeCapture-INFHLROL-FT4z4S1b.js} +1 -1
- package/dist/client/assets/sizeCapture-INFHLROL-FT4z4S1b.js.br +0 -0
- package/dist/client/assets/sizeCapture-INFHLROL-FT4z4S1b.js.gz +0 -0
- package/dist/client/assets/sparkles-BaGaevME.js +1 -0
- package/dist/client/assets/{stateDiagram-D77RDMKH-Dz836LqY.js → stateDiagram-D77RDMKH-DD0t9f84.js} +1 -1
- package/dist/client/assets/stateDiagram-D77RDMKH-DD0t9f84.js.br +0 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-DD0t9f84.js.gz +0 -0
- package/dist/client/assets/stateDiagram-v2-MP3YSRHH-BhvVVB56.js +1 -0
- package/dist/client/assets/{swimlanes-42K2YHIH-BK4KAY00.js → swimlanes-42K2YHIH-DcC8pGLx.js} +1 -1
- package/dist/client/assets/swimlanes-42K2YHIH-DcC8pGLx.js.br +0 -0
- package/dist/client/assets/swimlanes-42K2YHIH-DcC8pGLx.js.gz +0 -0
- package/dist/client/assets/swimlanesDiagram-VR7AAH4N-DSC5lz0i.js +8 -0
- package/dist/client/assets/{timeline-definition-24CTP7MA-DWgxTDhO.js → timeline-definition-24CTP7MA-DnwO_Pxd.js} +1 -1
- package/dist/client/assets/timeline-definition-24CTP7MA-DnwO_Pxd.js.br +0 -0
- package/dist/client/assets/timeline-definition-24CTP7MA-DnwO_Pxd.js.gz +0 -0
- package/dist/client/assets/triangle-alert-BCQO9Ap9.js +1 -0
- package/dist/client/assets/{vennDiagram-4TSXK5OY-BV1TIfOp.js → vennDiagram-4TSXK5OY-IVNSrZDM.js} +1 -1
- package/dist/client/assets/vennDiagram-4TSXK5OY-IVNSrZDM.js.br +0 -0
- package/dist/client/assets/vennDiagram-4TSXK5OY-IVNSrZDM.js.gz +0 -0
- package/dist/client/assets/{wardleyDiagram-VM6X3IG4-GYClC5F0.js → wardleyDiagram-VM6X3IG4-BnGetaQL.js} +1 -1
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-BnGetaQL.js.br +0 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-BnGetaQL.js.gz +0 -0
- package/dist/client/assets/{xychartDiagram-S5SC5T6Z-D1oKV5W0.js → xychartDiagram-S5SC5T6Z-C01eBN-W.js} +1 -1
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-C01eBN-W.js.br +0 -0
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-C01eBN-W.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/preview-shell.html +1 -0
- package/dist/client/preview-shell.js +20 -0
- package/dist/client/preview-shell.js.br +0 -0
- package/dist/client/preview-shell.js.gz +0 -0
- package/dist/client/sw-secure-api.js +132 -0
- package/dist/client/sw-secure-api.js.br +0 -0
- package/dist/client/sw-secure-api.js.gz +0 -0
- package/dist/client/sw.js +1 -1
- package/dist/client/sw.js.br +0 -0
- package/dist/client/sw.js.gz +0 -0
- package/dist/server/agent/collaborationCli.js +298 -0
- package/dist/server/agent/collaborationPrompt.js +12 -3
- package/dist/server/agent/collaborationProtocol.js +50 -0
- package/dist/server/agent/collaborationRoutes.js +86 -0
- package/dist/server/agent/collaborationStore.js +311 -20
- package/dist/server/cli.js +28 -163
- package/dist/server/entry.js +114 -51
- package/dist/server/federation/accessPolicy.js +122 -0
- package/dist/server/federation/authorization.js +226 -0
- package/dist/server/federation/bootstrapCors.js +23 -0
- package/dist/server/federation/cli.js +185 -0
- package/dist/server/federation/deviceNames.js +29 -0
- package/dist/server/federation/directRoutes.js +72 -0
- package/dist/server/federation/invitations.js +123 -0
- package/dist/server/federation/openAccess.js +25 -0
- package/dist/server/federation/originPolicy.js +19 -0
- package/dist/server/federation/packets.js +116 -0
- package/dist/server/federation/passwordBootstrap.js +86 -0
- package/dist/server/federation/passwordLoginRoutes.js +98 -0
- package/dist/server/federation/relay.js +346 -0
- package/dist/server/federation/requestContext.js +4 -0
- package/dist/server/federation/routeAccess.js +124 -0
- package/dist/server/federation/routeInvitations.js +160 -0
- package/dist/server/federation/runtime.js +537 -0
- package/dist/server/federation/secureProtocol.js +223 -0
- package/dist/server/federation/socketDuplex.js +42 -0
- package/dist/server/federation/streamLimits.js +5 -0
- package/dist/server/routes/filesystem.js +2 -1
- package/dist/server/routes/terminal.js +89 -79
- package/dist/server/utils/authProtection.js +13 -0
- package/dist/server/utils/csrfProtection.js +3 -0
- package/dist/server/utils/publicSecurity.js +8 -3
- package/package.json +10 -3
- package/runtime-manifest.json +4 -4
- package/dist/client/assets/ChangeWalkthroughPanel-DKj7F0ex.js +0 -2
- package/dist/client/assets/ChangeWalkthroughPanel-DKj7F0ex.js.br +0 -0
- package/dist/client/assets/ChangeWalkthroughPanel-DKj7F0ex.js.gz +0 -0
- package/dist/client/assets/DagPlayground-D2Wui4jd.js +0 -1
- package/dist/client/assets/DagPlayground-D2Wui4jd.js.br +0 -0
- package/dist/client/assets/DagPlayground-D2Wui4jd.js.gz +0 -0
- package/dist/client/assets/DiffLab-xuS6rtpv.js.br +0 -0
- package/dist/client/assets/DiffLab-xuS6rtpv.js.gz +0 -0
- package/dist/client/assets/DiffReview-CAwpqM_w.js +0 -1
- package/dist/client/assets/DiffReview-CAwpqM_w.js.br +0 -0
- package/dist/client/assets/DiffReview-CAwpqM_w.js.gz +0 -0
- package/dist/client/assets/DiffReviewLab-BM_uGdyI.js +0 -2
- package/dist/client/assets/DiffReviewLab-BM_uGdyI.js.br +0 -0
- package/dist/client/assets/DiffReviewLab-BM_uGdyI.js.gz +0 -0
- package/dist/client/assets/DiffViewer-BQGXUr8t.js.br +0 -0
- package/dist/client/assets/DiffViewer-BQGXUr8t.js.gz +0 -0
- package/dist/client/assets/ModelPreview-BfZzS1_d.js.br +0 -0
- package/dist/client/assets/ModelPreview-BfZzS1_d.js.gz +0 -0
- package/dist/client/assets/RightSidebar-Cdqt3C7V.js +0 -55
- package/dist/client/assets/RightSidebar-Cdqt3C7V.js.br +0 -0
- package/dist/client/assets/RightSidebar-Cdqt3C7V.js.gz +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-Dn_lmPcC.js.br +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-Dn_lmPcC.js.gz +0 -0
- package/dist/client/assets/app-DDCkFsNe.js +0 -175
- package/dist/client/assets/app-DDCkFsNe.js.br +0 -0
- package/dist/client/assets/app-DDCkFsNe.js.gz +0 -0
- package/dist/client/assets/arc-DUybtKGZ.js.br +0 -0
- package/dist/client/assets/arc-DUybtKGZ.js.gz +0 -0
- package/dist/client/assets/architectureDiagram-5GKGNRK7-CLEmOHBp.js.br +0 -0
- package/dist/client/assets/architectureDiagram-5GKGNRK7-CLEmOHBp.js.gz +0 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-O_XM5b9_.js.br +0 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-O_XM5b9_.js.gz +0 -0
- package/dist/client/assets/c4Diagram-7LVT6UL2-DlUEslcV.js.br +0 -0
- package/dist/client/assets/c4Diagram-7LVT6UL2-DlUEslcV.js.gz +0 -0
- package/dist/client/assets/channel-Bk1VkYvd.js +0 -1
- package/dist/client/assets/chunk-F27PBJKO-DkyD-X9z.js.br +0 -0
- package/dist/client/assets/chunk-F27PBJKO-DkyD-X9z.js.gz +0 -0
- package/dist/client/assets/chunk-IMKFNOWR-UvWxXAkh.js.br +0 -0
- package/dist/client/assets/chunk-IMKFNOWR-UvWxXAkh.js.gz +0 -0
- package/dist/client/assets/chunk-SVP7TREG-C4a1s0v4.js.br +0 -0
- package/dist/client/assets/chunk-SVP7TREG-C4a1s0v4.js.gz +0 -0
- package/dist/client/assets/chunk-TICWLB2K-hnmjYAl_.js.br +0 -0
- package/dist/client/assets/chunk-TICWLB2K-hnmjYAl_.js.gz +0 -0
- package/dist/client/assets/classDiagram-ZZMXUADV-_tUz3Cxt.js +0 -1
- package/dist/client/assets/classDiagram-v2-VYDZK3BY-_tUz3Cxt.js +0 -1
- package/dist/client/assets/cose-bilkent-JH36ORCC-CqY84fFp.js.br +0 -0
- package/dist/client/assets/cose-bilkent-JH36ORCC-CqY84fFp.js.gz +0 -0
- package/dist/client/assets/cynefin-OW5HDTMX-Bd14zuaD.js.br +0 -0
- package/dist/client/assets/cynefin-OW5HDTMX-Bd14zuaD.js.gz +0 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-DSHcNXDs.js.br +0 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-DSHcNXDs.js.gz +0 -0
- package/dist/client/assets/dagre-GXQ25YYZ-CIGWQYeP.js.br +0 -0
- package/dist/client/assets/dagre-GXQ25YYZ-CIGWQYeP.js.gz +0 -0
- package/dist/client/assets/diagram-S7CK7UJ4-C9Fg7OLN.js.br +0 -0
- package/dist/client/assets/diagram-S7CK7UJ4-C9Fg7OLN.js.gz +0 -0
- package/dist/client/assets/diagram-UQ7AKVKN-DoRhl7Fd.js.br +0 -0
- package/dist/client/assets/diagram-UQ7AKVKN-DoRhl7Fd.js.gz +0 -0
- package/dist/client/assets/diagram-VSXAHHWV-CI83g3iM.js.br +0 -0
- package/dist/client/assets/diagram-VSXAHHWV-CI83g3iM.js.gz +0 -0
- package/dist/client/assets/diagram-VX7I27RA-Dp2kP4PB.js.br +0 -0
- package/dist/client/assets/diagram-VX7I27RA-Dp2kP4PB.js.gz +0 -0
- package/dist/client/assets/diagram-Z3DM3KII-KtLMCM-V.js.br +0 -0
- package/dist/client/assets/diagram-Z3DM3KII-KtLMCM-V.js.gz +0 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-D_zseu1m.js.br +0 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-D_zseu1m.js.gz +0 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-DaWUl_Kz.js.gz +0 -0
- package/dist/client/assets/flowDiagram-HODETNUW-BMoJ46zJ.js.br +0 -0
- package/dist/client/assets/flowDiagram-HODETNUW-BMoJ46zJ.js.gz +0 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-lV_iVmoW.js.br +0 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-lV_iVmoW.js.gz +0 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-BVCT9Btd.js.br +0 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-BVCT9Btd.js.gz +0 -0
- package/dist/client/assets/index-CnKSUg74.css.br +0 -0
- package/dist/client/assets/index-CnKSUg74.css.gz +0 -0
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-Cyzfuoys.js.br +0 -0
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-Cyzfuoys.js.gz +0 -0
- package/dist/client/assets/journeyDiagram-3NMN7TZE-CEIPDhQF.js.br +0 -0
- package/dist/client/assets/journeyDiagram-3NMN7TZE-CEIPDhQF.js.gz +0 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-C8OulEOF.js.br +0 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-C8OulEOF.js.gz +0 -0
- package/dist/client/assets/layout-YN93qRzb.js.br +0 -0
- package/dist/client/assets/layout-YN93qRzb.js.gz +0 -0
- package/dist/client/assets/linear-D9T4dYh7.js.br +0 -0
- package/dist/client/assets/linear-D9T4dYh7.js.gz +0 -0
- package/dist/client/assets/mermaid.core-BdCvvCGV.js.br +0 -0
- package/dist/client/assets/mermaid.core-BdCvvCGV.js.gz +0 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-CSpES3Jh.js.br +0 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-CSpES3Jh.js.gz +0 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-6f4vAA1Q.js.br +0 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-6f4vAA1Q.js.gz +0 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-BC4Jd-I8.js.br +0 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-BC4Jd-I8.js.gz +0 -0
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-B0pnPaEN.js.br +0 -0
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-B0pnPaEN.js.gz +0 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-B-oOamZT.js.br +0 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-B-oOamZT.js.gz +0 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-DBSyxt0k.js.br +0 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-DBSyxt0k.js.gz +0 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-CcohzEZE.js.br +0 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-CcohzEZE.js.gz +0 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-D7KS6rq_.js.br +0 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-D7KS6rq_.js.gz +0 -0
- package/dist/client/assets/sizeCapture-INFHLROL-CaRXtn4g.js.br +0 -0
- package/dist/client/assets/sizeCapture-INFHLROL-CaRXtn4g.js.gz +0 -0
- package/dist/client/assets/sparkles-CUdUt4qy.js +0 -1
- package/dist/client/assets/stateDiagram-D77RDMKH-Dz836LqY.js.br +0 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-Dz836LqY.js.gz +0 -0
- package/dist/client/assets/stateDiagram-v2-MP3YSRHH-5V5Y3wwn.js +0 -1
- package/dist/client/assets/swimlanes-42K2YHIH-BK4KAY00.js.br +0 -0
- package/dist/client/assets/swimlanes-42K2YHIH-BK4KAY00.js.gz +0 -0
- package/dist/client/assets/swimlanesDiagram-VR7AAH4N-Sj-7YK2P.js +0 -8
- package/dist/client/assets/timeline-definition-24CTP7MA-DWgxTDhO.js.br +0 -0
- package/dist/client/assets/timeline-definition-24CTP7MA-DWgxTDhO.js.gz +0 -0
- package/dist/client/assets/vennDiagram-4TSXK5OY-BV1TIfOp.js.br +0 -0
- package/dist/client/assets/vennDiagram-4TSXK5OY-BV1TIfOp.js.gz +0 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-GYClC5F0.js.br +0 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-GYClC5F0.js.gz +0 -0
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-D1oKV5W0.js.br +0 -0
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-D1oKV5W0.js.gz +0 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdirSync, readFileSync, openSync, writeFileSync, fsyncSync, closeSync, renameSync, unlinkSync } from 'node:fs';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
const id = (v) => typeof v === 'string' && v.length > 0 && v.length <= 512 && !/[\s\x00-\x1f]/.test(v);
|
|
5
|
+
const action = (v) => typeof v === 'string' && (v === 'service:*' || /^[a-z][a-z0-9_-]*\.[a-z][a-z0-9_-]*$/.test(v));
|
|
6
|
+
const object = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
7
|
+
function assert(value, message) { if (!value)
|
|
8
|
+
throw new Error(`Invalid authorization: ${message}`); }
|
|
9
|
+
function keys(v, allowed) { assert(Object.keys(v).every(k => allowed.includes(k)), 'unknown field'); }
|
|
10
|
+
function ids(v) { return Array.isArray(v) && v.length > 0 && v.length <= 10000 && v.every(id) && new Set(v).size === v.length; }
|
|
11
|
+
function validateInput(v) {
|
|
12
|
+
assert(object(v), 'grant object');
|
|
13
|
+
assert(id(v.subjectId), 'subject identity');
|
|
14
|
+
assert(object(v.scope), 'scope');
|
|
15
|
+
if (v.scope.kind === 'service')
|
|
16
|
+
keys(v.scope, ['kind']);
|
|
17
|
+
else {
|
|
18
|
+
assert(v.scope.kind === 'sessions' && ids(v.scope.sessionIds), 'session scope');
|
|
19
|
+
keys(v.scope, ['kind', 'sessionIds']);
|
|
20
|
+
}
|
|
21
|
+
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
|
+
if (v.scope.kind === 'sessions')
|
|
23
|
+
assert(v.actions.every(a => a.startsWith('session.')), 'session scope cannot grant service or file capabilities');
|
|
24
|
+
assert(v.expiresAt === undefined || (Number.isSafeInteger(v.expiresAt) && Number(v.expiresAt) > 0), 'expiry');
|
|
25
|
+
assert(v.canDelegate === undefined || typeof v.canDelegate === 'boolean', 'delegation flag');
|
|
26
|
+
if (v.conditions !== undefined) {
|
|
27
|
+
assert(object(v.conditions), 'conditions');
|
|
28
|
+
keys(v.conditions, ['entryServiceIds', 'relayIds']);
|
|
29
|
+
for (const list of Object.values(v.conditions))
|
|
30
|
+
assert(ids(list), 'condition identities');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function isSubset(child, parent) {
|
|
34
|
+
if (child.serviceId !== parent.serviceId || !parent.canDelegate)
|
|
35
|
+
return false;
|
|
36
|
+
if (parent.expiresAt !== undefined && (child.expiresAt === undefined || child.expiresAt > parent.expiresAt))
|
|
37
|
+
return false;
|
|
38
|
+
if (parent.scope.kind === 'sessions' && (child.scope.kind !== 'sessions' || !child.scope.sessionIds.every(s => parent.scope.kind === 'sessions' && parent.scope.sessionIds.includes(s))))
|
|
39
|
+
return false;
|
|
40
|
+
if (!parent.actions.includes('service:*') && !child.actions.every(a => parent.actions.includes(a)))
|
|
41
|
+
return false;
|
|
42
|
+
for (const key of ['entryServiceIds', 'relayIds']) {
|
|
43
|
+
const limit = parent.conditions?.[key];
|
|
44
|
+
if (limit && (!child.conditions?.[key] || !child.conditions[key].every(x => limit.includes(x))))
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
function active(g, grants, now, seen = new Set(), passwordCredential) {
|
|
50
|
+
if (seen.has(g.id) || g.createdAt > now || g.revokedAt !== undefined || (g.expiresAt !== undefined && g.expiresAt <= now))
|
|
51
|
+
return false;
|
|
52
|
+
if (g.passwordCredential && g.passwordCredential !== passwordCredential)
|
|
53
|
+
return false;
|
|
54
|
+
seen.add(g.id);
|
|
55
|
+
if (!g.parentGrantId)
|
|
56
|
+
return true;
|
|
57
|
+
const parent = grants.find(p => p.id === g.parentGrantId);
|
|
58
|
+
return !!parent && isSubset(g, parent) && active(parent, grants, now, seen, passwordCredential);
|
|
59
|
+
}
|
|
60
|
+
/** Target-service authority. Root mutations must only be exposed to authenticated administrators.
|
|
61
|
+
* Every operation reloads disk; synchronous atomic writes support one owning server process.
|
|
62
|
+
* Do not share this file between concurrently writing processes. */
|
|
63
|
+
export class AuthorizationStore {
|
|
64
|
+
options;
|
|
65
|
+
now;
|
|
66
|
+
constructor(options) {
|
|
67
|
+
this.options = options;
|
|
68
|
+
assert(id(options.serviceId), 'service identity');
|
|
69
|
+
this.now = options.now ?? Date.now;
|
|
70
|
+
this.read();
|
|
71
|
+
}
|
|
72
|
+
read() {
|
|
73
|
+
let raw;
|
|
74
|
+
try {
|
|
75
|
+
raw = readFileSync(this.options.filePath, 'utf8');
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error.code === 'ENOENT')
|
|
79
|
+
return [];
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
const data = JSON.parse(raw);
|
|
83
|
+
assert(object(data) && data.version === 1 && data.serviceId === this.options.serviceId && Array.isArray(data.grants), 'store header');
|
|
84
|
+
keys(data, ['version', 'serviceId', 'grants']);
|
|
85
|
+
const grants = [];
|
|
86
|
+
for (const value of data.grants) {
|
|
87
|
+
validateInput(value);
|
|
88
|
+
const g = value;
|
|
89
|
+
keys(g, ['id', 'serviceId', 'createdAt', 'parentGrantId', 'revokedAt', 'subjectId', 'scope', 'actions', 'expiresAt', 'canDelegate', 'conditions', 'passwordCredential']);
|
|
90
|
+
assert(id(g.id) && g.serviceId === this.options.serviceId && Number.isSafeInteger(g.createdAt) && g.createdAt >= 0, 'grant metadata');
|
|
91
|
+
assert(g.parentGrantId === undefined || id(g.parentGrantId), 'parent identity');
|
|
92
|
+
assert(g.revokedAt === undefined || (Number.isSafeInteger(g.revokedAt) && g.revokedAt >= 0), 'revocation');
|
|
93
|
+
assert(g.passwordCredential === undefined || /^[a-f0-9]{64}$/.test(g.passwordCredential), 'password credential');
|
|
94
|
+
assert(!grants.some(p => p.id === g.id), 'duplicate grant');
|
|
95
|
+
grants.push(g);
|
|
96
|
+
}
|
|
97
|
+
return grants;
|
|
98
|
+
}
|
|
99
|
+
save(grants) {
|
|
100
|
+
mkdirSync(dirname(this.options.filePath), { recursive: true, mode: 0o700 });
|
|
101
|
+
const temp = `${this.options.filePath}.${randomUUID()}.tmp`;
|
|
102
|
+
let fd;
|
|
103
|
+
try {
|
|
104
|
+
fd = openSync(temp, 'wx', 0o600);
|
|
105
|
+
writeFileSync(fd, JSON.stringify({ version: 1, serviceId: this.options.serviceId, grants }, null, 2));
|
|
106
|
+
fsyncSync(fd);
|
|
107
|
+
closeSync(fd);
|
|
108
|
+
fd = undefined;
|
|
109
|
+
renameSync(temp, this.options.filePath);
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
if (fd !== undefined)
|
|
113
|
+
closeSync(fd);
|
|
114
|
+
try {
|
|
115
|
+
unlinkSync(temp);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (error.code !== 'ENOENT')
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
create(input) {
|
|
124
|
+
validateInput(input);
|
|
125
|
+
keys(input, ['subjectId', 'scope', 'actions', 'expiresAt', 'canDelegate', 'conditions']);
|
|
126
|
+
const now = this.now();
|
|
127
|
+
assert(input.expiresAt === undefined || input.expiresAt > now, 'already expired');
|
|
128
|
+
return { ...structuredClone(input), id: randomUUID(), serviceId: this.options.serviceId, createdAt: now };
|
|
129
|
+
}
|
|
130
|
+
/** Trusted target administrator only; never authorize this from a relay's identity. */
|
|
131
|
+
grant(input) {
|
|
132
|
+
const grants = this.read();
|
|
133
|
+
const grant = this.create(input);
|
|
134
|
+
grants.push(grant);
|
|
135
|
+
this.save(grants);
|
|
136
|
+
return structuredClone(grant);
|
|
137
|
+
}
|
|
138
|
+
/** Only call after a successful mutually authenticated password proof. */
|
|
139
|
+
grantPassword(subjectId) {
|
|
140
|
+
const passwordCredential = this.options.passwordCredential?.();
|
|
141
|
+
assert(passwordCredential && /^[a-f0-9]{64}$/.test(passwordCredential), 'password unavailable');
|
|
142
|
+
const grants = this.read();
|
|
143
|
+
for (const grant of grants)
|
|
144
|
+
if (grant.subjectId === subjectId && grant.passwordCredential)
|
|
145
|
+
grant.revokedAt = this.now();
|
|
146
|
+
const grant = { ...this.create({ subjectId, scope: { kind: 'service' }, actions: ['service:*'], canDelegate: true, expiresAt: this.now() + 30 * 24 * 60 * 60 * 1000 }), passwordCredential };
|
|
147
|
+
grants.push(grant);
|
|
148
|
+
this.save(grants);
|
|
149
|
+
return structuredClone(grant);
|
|
150
|
+
}
|
|
151
|
+
delegate(parentGrantId, authenticatedSubjectId, input) {
|
|
152
|
+
const grants = this.read();
|
|
153
|
+
const parent = grants.find(g => g.id === parentGrantId);
|
|
154
|
+
assert(parent && parent.subjectId === authenticatedSubjectId && active(parent, grants, this.now(), new Set(), this.options.passwordCredential?.()), 'inactive or unowned parent');
|
|
155
|
+
const child = { ...this.create(input), parentGrantId };
|
|
156
|
+
assert(isSubset(child, parent), 'delegation exceeds parent');
|
|
157
|
+
grants.push(child);
|
|
158
|
+
this.save(grants);
|
|
159
|
+
return structuredClone(child);
|
|
160
|
+
}
|
|
161
|
+
/** Trusted target administrator only. Descendants become ineffective immediately on next check. */
|
|
162
|
+
revoke(grantId) {
|
|
163
|
+
const grants = this.read();
|
|
164
|
+
const grant = grants.find(g => g.id === grantId);
|
|
165
|
+
if (!grant || grant.revokedAt !== undefined)
|
|
166
|
+
return false;
|
|
167
|
+
grant.revokedAt = this.now();
|
|
168
|
+
this.save(grants);
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
list() { return this.read(); }
|
|
172
|
+
listEffective(request) {
|
|
173
|
+
if (!request || !id(request.subjectId) || request.serviceId !== this.options.serviceId)
|
|
174
|
+
return [];
|
|
175
|
+
let grants;
|
|
176
|
+
try {
|
|
177
|
+
grants = this.read();
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
const now = this.now();
|
|
183
|
+
return grants.filter(g => g.subjectId === request.subjectId && active(g, grants, now, new Set(), this.options.passwordCredential?.())
|
|
184
|
+
&& (!g.conditions?.entryServiceIds || (!!request.entryServiceId && g.conditions.entryServiceIds.includes(request.entryServiceId)))
|
|
185
|
+
&& (!g.conditions?.relayIds || (!!request.relayId && g.conditions.relayIds.includes(request.relayId))));
|
|
186
|
+
}
|
|
187
|
+
/** Tests the actual wildcard, not an invented action that custom grants could allow. */
|
|
188
|
+
hasFullServiceAccess(request) {
|
|
189
|
+
return this.listEffective(request).some(g => g.scope.kind === 'service' && g.actions.includes('service:*'));
|
|
190
|
+
}
|
|
191
|
+
authorize(request) {
|
|
192
|
+
const deny = { allowed: false, grantIds: [] };
|
|
193
|
+
if (!request || !id(request.subjectId) || request.serviceId !== this.options.serviceId || !action(request.action) || request.action === 'service:*')
|
|
194
|
+
return deny;
|
|
195
|
+
if (request.action.startsWith('session.') && !id(request.sessionId))
|
|
196
|
+
return deny;
|
|
197
|
+
if (request.sessionId !== undefined && !id(request.sessionId))
|
|
198
|
+
return deny;
|
|
199
|
+
if (request.entryServiceId !== undefined && !id(request.entryServiceId))
|
|
200
|
+
return deny;
|
|
201
|
+
if (request.relayId !== undefined && !id(request.relayId))
|
|
202
|
+
return deny;
|
|
203
|
+
let grants;
|
|
204
|
+
try {
|
|
205
|
+
grants = this.read();
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return deny;
|
|
209
|
+
}
|
|
210
|
+
const now = this.now();
|
|
211
|
+
const grantIds = grants.filter(g => {
|
|
212
|
+
if (g.subjectId !== request.subjectId || !active(g, grants, now, new Set(), this.options.passwordCredential?.()))
|
|
213
|
+
return false;
|
|
214
|
+
if (!g.actions.includes('service:*') && !g.actions.includes(request.action))
|
|
215
|
+
return false;
|
|
216
|
+
if (g.scope.kind === 'sessions' && (!request.action.startsWith('session.') || !request.sessionId || !g.scope.sessionIds.includes(request.sessionId)))
|
|
217
|
+
return false;
|
|
218
|
+
if (g.conditions?.entryServiceIds && (!request.entryServiceId || !g.conditions.entryServiceIds.includes(request.entryServiceId)))
|
|
219
|
+
return false;
|
|
220
|
+
if (g.conditions?.relayIds && (!request.relayId || !g.conditions.relayIds.includes(request.relayId)))
|
|
221
|
+
return false;
|
|
222
|
+
return true;
|
|
223
|
+
}).map(g => g.id);
|
|
224
|
+
return { allowed: grantIds.length > 0, grantIds };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { isAllowedOrigin } from '../utils/requestSecurity.js';
|
|
2
|
+
import { secureChannelOriginAllowed } from './originPolicy.js';
|
|
3
|
+
/** These endpoints exchange explicit password proofs, never cookies or grants
|
|
4
|
+
* from the caller's browser session. Business routes retain their strict policy. */
|
|
5
|
+
export const bootstrapCors = (req, res, next) => {
|
|
6
|
+
const origin = req.headers.origin;
|
|
7
|
+
if (!secureChannelOriginAllowed(origin, () => isAllowedOrigin(origin, req.headers.host))) {
|
|
8
|
+
res.status(403).json({ error: 'Origin is not allowed', code: 'ORIGIN_NOT_ALLOWED' });
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
12
|
+
if (origin) {
|
|
13
|
+
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
14
|
+
res.vary('Origin');
|
|
15
|
+
}
|
|
16
|
+
if (req.method === 'OPTIONS') {
|
|
17
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
18
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
19
|
+
res.status(204).end();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
next();
|
|
23
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import WebSocket from 'ws';
|
|
5
|
+
import QRCode from 'qrcode';
|
|
6
|
+
import { RelayClient } from './relay.js';
|
|
7
|
+
function secureEndpoint(value, endpoint) {
|
|
8
|
+
const url = new URL(value);
|
|
9
|
+
if (!['https:', 'wss:'].includes(url.protocol) || url.username || url.password || url.search || url.hash)
|
|
10
|
+
throw new Error('Federation relay URLs must use HTTPS/WSS without credentials, query or fragment');
|
|
11
|
+
if (url.pathname !== '/' && url.pathname !== endpoint)
|
|
12
|
+
throw new Error('Federation relay URL must be a service origin or the exact tunnel endpoint');
|
|
13
|
+
url.protocol = 'wss:';
|
|
14
|
+
url.pathname = endpoint;
|
|
15
|
+
return url.toString();
|
|
16
|
+
}
|
|
17
|
+
export function parseRelayConfig(value) {
|
|
18
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
19
|
+
throw new Error('Invalid relay config');
|
|
20
|
+
const v = value;
|
|
21
|
+
if (Object.keys(v).some(k => !['entryUrl', 'relayToken', 'caPath', 'targets'].includes(k)) || typeof v.entryUrl !== 'string' || typeof v.relayToken !== 'string' || v.relayToken.length < 32 || /[\r\n]/.test(v.relayToken) || !Array.isArray(v.targets) || !v.targets.length || v.targets.length > 64 || (v.caPath !== undefined && typeof v.caPath !== 'string'))
|
|
22
|
+
throw new Error('Invalid relay config fields');
|
|
23
|
+
secureEndpoint(v.entryUrl, '/api/federation/relay');
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
for (const target of v.targets) {
|
|
26
|
+
if (!target || typeof target !== 'object' || Array.isArray(target) || Object.keys(target).some(k => !['serviceId', 'url', 'caPath'].includes(k)) || typeof target.serviceId !== 'string' || !/^[a-zA-Z0-9_.:-]{1,160}$/.test(target.serviceId) || typeof target.url !== 'string' || (target.caPath !== undefined && typeof target.caPath !== 'string') || seen.has(target.serviceId))
|
|
27
|
+
throw new Error('Invalid or duplicate relay target');
|
|
28
|
+
secureEndpoint(target.url, '/api/federation/secure');
|
|
29
|
+
seen.add(target.serviceId);
|
|
30
|
+
}
|
|
31
|
+
return structuredClone(v);
|
|
32
|
+
}
|
|
33
|
+
function connect(url, ca, sockets, token, readyHandshake = false) {
|
|
34
|
+
return new Promise((accept, reject) => {
|
|
35
|
+
const ws = new WebSocket(url, { ca, rejectUnauthorized: true, followRedirects: false, handshakeTimeout: 10_000, maxPayload: 256 * 1024, ...(token ? { headers: { Authorization: `Bearer ${token}` } } : {}) });
|
|
36
|
+
sockets.add(ws);
|
|
37
|
+
ws.once('close', () => sockets.delete(ws));
|
|
38
|
+
const failed = () => { ws.terminate(); reject(new Error('Secure relay connection failed')); };
|
|
39
|
+
ws.once('error', failed);
|
|
40
|
+
ws.once('close', failed);
|
|
41
|
+
ws.once('open', () => {
|
|
42
|
+
ws.off('error', failed);
|
|
43
|
+
ws.off('close', failed);
|
|
44
|
+
ws.on('error', () => ws.terminate());
|
|
45
|
+
if (readyHandshake)
|
|
46
|
+
void awaitReady(ws).then(() => accept(ws), reject);
|
|
47
|
+
else
|
|
48
|
+
accept(ws);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
async function awaitReady(ws) {
|
|
53
|
+
return new Promise((accept, reject) => {
|
|
54
|
+
const timer = setTimeout(() => fail(), 10_000);
|
|
55
|
+
const cleanup = () => { clearTimeout(timer); ws.off('message', ready); ws.off('close', fail); };
|
|
56
|
+
const fail = () => { cleanup(); ws.terminate(); reject(new Error('Relay authentication failed')); };
|
|
57
|
+
const ready = (raw) => {
|
|
58
|
+
try {
|
|
59
|
+
const frame = JSON.parse(raw.toString());
|
|
60
|
+
if (frame.type !== 'ready') {
|
|
61
|
+
fail();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
cleanup();
|
|
65
|
+
accept();
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
fail();
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
ws.once('message', ready);
|
|
72
|
+
ws.once('close', fail);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
export function pairingInviteUrl(serviceId, code, value) {
|
|
76
|
+
const url = new URL(value);
|
|
77
|
+
if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password || url.search || url.hash || url.pathname !== '/')
|
|
78
|
+
throw new Error('配对地址必须是服务的 HTTP(S) 根地址,不含账号、参数或片段。');
|
|
79
|
+
if (url.protocol === 'http:' && !['localhost', '127.0.0.1', '[::1]'].includes(url.hostname))
|
|
80
|
+
throw new Error('远程配对地址必须使用 HTTPS。');
|
|
81
|
+
if (!/^[A-Za-z0-9_-]{43}$/.test(code) || !/^[A-Za-z0-9_.:-]{1,160}$/.test(serviceId))
|
|
82
|
+
throw new Error('Invalid pairing information');
|
|
83
|
+
const origin = url.origin;
|
|
84
|
+
url.hash = 'termdock-invite=' + Buffer.from(JSON.stringify({ v: 1, serviceId, code, entryUrl: origin, serviceUrl: origin })).toString('base64url');
|
|
85
|
+
return url.href;
|
|
86
|
+
}
|
|
87
|
+
export async function runFederationCli(args) {
|
|
88
|
+
try {
|
|
89
|
+
if (args[0] === '--federation-pairing') {
|
|
90
|
+
let explicitUrl;
|
|
91
|
+
let json = false;
|
|
92
|
+
for (let i = 1; i < args.length; i++) {
|
|
93
|
+
if (args[i] === '--json' && !json)
|
|
94
|
+
json = true;
|
|
95
|
+
else if (args[i] === '--url' && explicitUrl === undefined && args[i + 1])
|
|
96
|
+
explicitUrl = args[++i];
|
|
97
|
+
else
|
|
98
|
+
throw new Error('Usage: termdock --federation-pairing [--url https://service:port] [--json]');
|
|
99
|
+
}
|
|
100
|
+
const file = resolve(homedir(), '.termdock/federation/pairing.json');
|
|
101
|
+
if (process.platform !== 'win32' && (statSync(file).mode & 0o077) !== 0)
|
|
102
|
+
throw new Error('配对文件权限异常,请先 chmod 600 配对文件。');
|
|
103
|
+
const data = JSON.parse(readFileSync(file, 'utf8'));
|
|
104
|
+
if (!data || typeof data.serviceId !== 'string' || typeof data.pairingCode !== 'string')
|
|
105
|
+
throw new Error('Invalid pairing file; start the service to initialize pairing');
|
|
106
|
+
if (json) {
|
|
107
|
+
process.stdout.write(`${JSON.stringify({ serviceId: data.serviceId, pairingCode: data.pairingCode, ...(explicitUrl ? { url: explicitUrl } : typeof data.url === 'string' ? { url: data.url } : {}) })}\n`);
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
let serviceUrl = explicitUrl ?? (typeof data.url === 'string' ? data.url : undefined);
|
|
111
|
+
if (!serviceUrl) {
|
|
112
|
+
try {
|
|
113
|
+
const state = JSON.parse(readFileSync(resolve(homedir(), '.termdock/server.json'), 'utf8'));
|
|
114
|
+
serviceUrl = typeof state.lanUrl === 'string' ? state.lanUrl : typeof state.localUrl === 'string' ? state.localUrl : undefined;
|
|
115
|
+
}
|
|
116
|
+
catch { /* Require an explicit usable service URL below. */ }
|
|
117
|
+
}
|
|
118
|
+
if (!serviceUrl)
|
|
119
|
+
throw new Error('请添加 --url https://服务地址:端口,生成可直接打开的配对链接。');
|
|
120
|
+
const invitation = pairingInviteUrl(data.serviceId, data.pairingCode, serviceUrl);
|
|
121
|
+
process.stdout.write(`用新设备打开链接或扫描二维码,即可一次性配对本服务:\n${invitation}\n`);
|
|
122
|
+
process.stdout.write(await QRCode.toString(invitation, { type: 'terminal', small: true }));
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
if (args[0] !== '--federation-relay' || args.length !== 2)
|
|
126
|
+
throw new Error('Usage: termdock --federation-pairing [--url https://service:port] [--json] | termdock --federation-relay <config.json>');
|
|
127
|
+
const configPath = resolve(args[1]);
|
|
128
|
+
if (process.platform !== 'win32' && (statSync(configPath).mode & 0o077) !== 0)
|
|
129
|
+
throw new Error('Relay config contains a token: chmod 600 the config file first');
|
|
130
|
+
const config = parseRelayConfig(JSON.parse(readFileSync(configPath, 'utf8')));
|
|
131
|
+
const loadCa = (file) => file ? readFileSync(resolve(dirname(configPath), file)) : undefined;
|
|
132
|
+
const entryCa = loadCa(config.caPath);
|
|
133
|
+
const targets = config.targets.map(t => ({ ...t, ca: loadCa(t.caPath ?? config.caPath) }));
|
|
134
|
+
const sockets = new Set();
|
|
135
|
+
let stopped = false;
|
|
136
|
+
let client;
|
|
137
|
+
let wake;
|
|
138
|
+
const stop = () => { stopped = true; wake?.(); client?.close(); for (const socket of sockets)
|
|
139
|
+
socket.terminate(); };
|
|
140
|
+
process.once('SIGINT', stop);
|
|
141
|
+
process.once('SIGTERM', stop);
|
|
142
|
+
let delay = 1000;
|
|
143
|
+
try {
|
|
144
|
+
while (!stopped) {
|
|
145
|
+
try {
|
|
146
|
+
const upstream = await connect(secureEndpoint(config.entryUrl, '/api/federation/relay'), entryCa, sockets, config.relayToken, true);
|
|
147
|
+
if (stopped) {
|
|
148
|
+
upstream.terminate();
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
client = new RelayClient(upstream, { targets: new Map(targets.map(t => [t.serviceId, () => connect(secureEndpoint(t.url, '/api/federation/secure'), t.ca, sockets)])) });
|
|
152
|
+
process.stderr.write('加密中继已连接。\n');
|
|
153
|
+
delay = 1000;
|
|
154
|
+
await new Promise(done => { if (upstream.readyState !== WebSocket.OPEN)
|
|
155
|
+
done();
|
|
156
|
+
else
|
|
157
|
+
upstream.once('close', () => done()); });
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
if (!stopped)
|
|
161
|
+
process.stderr.write('中继连接失败或已断开,将重试;请检查授权和证书。\n');
|
|
162
|
+
}
|
|
163
|
+
client?.close();
|
|
164
|
+
client = undefined;
|
|
165
|
+
for (const socket of sockets)
|
|
166
|
+
socket.terminate();
|
|
167
|
+
if (!stopped)
|
|
168
|
+
await new Promise(done => { const timer = setTimeout(() => { wake = undefined; done(); }, delay); wake = () => { clearTimeout(timer); wake = undefined; done(); }; });
|
|
169
|
+
delay = Math.min(delay * 2, 30_000);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
stop();
|
|
174
|
+
process.off('SIGINT', stop);
|
|
175
|
+
process.off('SIGTERM', stop);
|
|
176
|
+
}
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
// Config parsing and network failures must not echo the config or its token.
|
|
181
|
+
const message = error instanceof Error && !('code' in error) && !(error instanceof SyntaxError) ? error.message : '无法读取有效配置或配对信息。';
|
|
182
|
+
process.stderr.write(`${message}\n`);
|
|
183
|
+
return 1;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, renameSync } from 'node:fs';
|
|
2
|
+
/** Display metadata only; never participates in authorization. */
|
|
3
|
+
export class DeviceNames {
|
|
4
|
+
file;
|
|
5
|
+
constructor(file) {
|
|
6
|
+
this.file = file;
|
|
7
|
+
}
|
|
8
|
+
list() {
|
|
9
|
+
try {
|
|
10
|
+
return Object.fromEntries(Object.entries(JSON.parse(readFileSync(this.file, 'utf8'))).filter(([, name]) => typeof name === 'string'));
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (error.code === 'ENOENT')
|
|
14
|
+
return {};
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
set(subjectId, name, onlyIfMissing) {
|
|
19
|
+
if (typeof name !== 'string' || !name.trim() || name.length > 80 || /[\x00-\x1f\x7f]/.test(name))
|
|
20
|
+
throw new Error('INVALID_DEVICE_NAME');
|
|
21
|
+
const names = this.list();
|
|
22
|
+
if (onlyIfMissing && Object.hasOwn(names, subjectId))
|
|
23
|
+
return names[subjectId];
|
|
24
|
+
const next = Object.fromEntries([...Object.entries(names).filter(([id]) => id !== subjectId), [subjectId, name.trim()]]);
|
|
25
|
+
writeFileSync(`${this.file}.tmp`, JSON.stringify(next), { mode: 0o600 });
|
|
26
|
+
renameSync(`${this.file}.tmp`, this.file);
|
|
27
|
+
return name.trim();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createIdentity, secureConnection } from './secureProtocol.js';
|
|
2
|
+
import { socketDuplex } from './socketDuplex.js';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import WebSocket from 'ws';
|
|
6
|
+
function endpoint(config) {
|
|
7
|
+
if (!config || typeof config !== 'object' || Object.keys(config).some(key => !['serviceId', 'url', 'caPath'].includes(key)) || !/^[a-zA-Z0-9_.:-]{1,160}$/.test(config.serviceId) || typeof config.url !== 'string' || (config.caPath !== undefined && typeof config.caPath !== 'string'))
|
|
8
|
+
throw new Error('Invalid direct target configuration');
|
|
9
|
+
const url = new URL(config.url);
|
|
10
|
+
if (!['https:', 'wss:'].includes(url.protocol) || url.username || url.password || url.search || url.hash || !['/', '/api/federation/secure'].includes(url.pathname))
|
|
11
|
+
throw new Error('Direct targets require a verified HTTPS/WSS origin');
|
|
12
|
+
url.protocol = 'wss:';
|
|
13
|
+
url.pathname = '/api/federation/secure';
|
|
14
|
+
return url.toString();
|
|
15
|
+
}
|
|
16
|
+
/** Registers only administrator-configured destinations. TLS validation and redirect
|
|
17
|
+
* refusal apply even though the inner session also has end-to-end encryption. */
|
|
18
|
+
export function attachRegisteredDirectTargets(router, configs, baseDir = process.cwd()) {
|
|
19
|
+
if (!Array.isArray(configs) || configs.length > 64)
|
|
20
|
+
throw new Error('Invalid direct target registry');
|
|
21
|
+
const ids = new Set();
|
|
22
|
+
const targets = configs.map(config => {
|
|
23
|
+
const url = endpoint(config);
|
|
24
|
+
if (ids.has(config.serviceId))
|
|
25
|
+
throw new Error('Duplicate direct target');
|
|
26
|
+
ids.add(config.serviceId);
|
|
27
|
+
return { serviceId: config.serviceId, url, ca: config.caPath ? readFileSync(resolve(baseDir, config.caPath)) : undefined };
|
|
28
|
+
});
|
|
29
|
+
let stopped = false;
|
|
30
|
+
const sockets = new Set();
|
|
31
|
+
const cleanups = [];
|
|
32
|
+
const cleanup = () => { stopped = true; for (const close of cleanups.splice(0))
|
|
33
|
+
close(); for (const ws of sockets)
|
|
34
|
+
ws.terminate(); };
|
|
35
|
+
try {
|
|
36
|
+
for (const target of targets)
|
|
37
|
+
cleanups.push(router.registerDirect(target.serviceId, () => new Promise((accept, reject) => {
|
|
38
|
+
if (stopped) {
|
|
39
|
+
reject(new Error('Direct target stopped'));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const ws = new WebSocket(target.url, { ca: target.ca, rejectUnauthorized: true, followRedirects: false, handshakeTimeout: 10000, maxPayload: 256 * 1024 });
|
|
43
|
+
sockets.add(ws);
|
|
44
|
+
ws.once('close', () => sockets.delete(ws));
|
|
45
|
+
const failed = () => { reject(new Error('Direct target unavailable')); ws.terminate(); };
|
|
46
|
+
ws.once('error', failed);
|
|
47
|
+
ws.once('close', failed);
|
|
48
|
+
ws.once('open', () => { ws.off('error', failed); ws.off('close', failed); ws.on('error', () => ws.terminate()); if (stopped)
|
|
49
|
+
failed();
|
|
50
|
+
else
|
|
51
|
+
accept(ws); });
|
|
52
|
+
})));
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
cleanup();
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
return cleanup;
|
|
59
|
+
}
|
|
60
|
+
/** An administrator enabling a new backup first proves the target's pinned
|
|
61
|
+
* identity; HTTPS validation remains enabled throughout. */
|
|
62
|
+
export async function verifyDirectTarget(config) {
|
|
63
|
+
const ws = new WebSocket(endpoint(config), { rejectUnauthorized: true, followRedirects: false, handshakeTimeout: 5000, maxPayload: 256 * 1024 });
|
|
64
|
+
const duplex = socketDuplex(ws);
|
|
65
|
+
try {
|
|
66
|
+
await new Promise((resolve, reject) => { ws.once('open', resolve); ws.once('error', reject); ws.once('close', () => reject(new Error('Target closed'))); });
|
|
67
|
+
await secureConnection({ identity: await createIdentity(), initiator: true, targetPinnedPeerId: config.serviceId, duplex, signal: AbortSignal.timeout(5000) });
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
ws.close();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdirSync, readFileSync, openSync, writeFileSync, fsyncSync, closeSync, renameSync, unlinkSync, chmodSync } from 'node:fs';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
const TTL = 10 * 60 * 1000;
|
|
5
|
+
const actions = new Set(['session.view', 'session.input', 'session.resize', 'session.terminate']);
|
|
6
|
+
const identity = (v) => typeof v === 'string' && v.length > 0 && v.length <= 512 && !/[\s\x00-\x1f]/.test(v);
|
|
7
|
+
const record = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
8
|
+
function requireValid(ok) { if (!ok)
|
|
9
|
+
throw new Error('INVALID_INVITATION'); }
|
|
10
|
+
export function validateInvitation(input) {
|
|
11
|
+
requireValid(record(input) && Object.keys(input).every(k => ['scope', 'actions', 'label', 'expiresAt'].includes(k)));
|
|
12
|
+
requireValid(record(input.scope) && Array.isArray(input.actions) && input.actions.length > 0 && new Set(input.actions).size === input.actions.length);
|
|
13
|
+
const scope = input.scope;
|
|
14
|
+
if (scope.kind === 'service') {
|
|
15
|
+
requireValid(Object.keys(scope).length === 1 && ((input.actions.length === 1 && input.actions[0] === 'service:*') || input.actions.every(a => typeof a === 'string' && actions.has(a))));
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
requireValid(scope.kind === 'sessions' && Object.keys(scope).every(k => ['kind', 'sessionIds'].includes(k))
|
|
19
|
+
&& Array.isArray(scope.sessionIds) && scope.sessionIds.length > 0 && scope.sessionIds.length <= 256
|
|
20
|
+
&& scope.sessionIds.every(identity) && new Set(scope.sessionIds).size === scope.sessionIds.length
|
|
21
|
+
&& input.actions.every(a => typeof a === 'string' && actions.has(a)));
|
|
22
|
+
}
|
|
23
|
+
requireValid(input.label === undefined || (typeof input.label === 'string' && input.label.trim().length > 0 && input.label.length <= 80 && !/[\x00-\x1f\x7f]/.test(input.label)));
|
|
24
|
+
requireValid(input.expiresAt === undefined || (Number.isSafeInteger(input.expiresAt) && Number(input.expiresAt) > 0));
|
|
25
|
+
}
|
|
26
|
+
/** Single owning process, synchronous consume-before-grant: a crash may burn an invite,
|
|
27
|
+
* but can never redeem it twice. Raw invitation codes are never persisted. */
|
|
28
|
+
export class InvitationStore {
|
|
29
|
+
options;
|
|
30
|
+
now;
|
|
31
|
+
constructor(options) {
|
|
32
|
+
this.options = options;
|
|
33
|
+
requireValid(identity(options.serviceId));
|
|
34
|
+
this.now = options.now ?? Date.now;
|
|
35
|
+
this.read();
|
|
36
|
+
}
|
|
37
|
+
hash(code) { return createHash('sha256').update(`termdock-invitation-v1:${this.options.serviceId}:`).update(code).digest('hex'); }
|
|
38
|
+
read() {
|
|
39
|
+
let raw;
|
|
40
|
+
try {
|
|
41
|
+
raw = readFileSync(this.options.filePath, 'utf8');
|
|
42
|
+
chmodSync(this.options.filePath, 0o600);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (error.code === 'ENOENT')
|
|
46
|
+
return [];
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
const data = JSON.parse(raw);
|
|
50
|
+
requireValid(record(data) && data.version === 1 && data.serviceId === this.options.serviceId && Array.isArray(data.invitations) && data.invitations.length <= 2048);
|
|
51
|
+
const seen = new Set();
|
|
52
|
+
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 }) });
|
|
56
|
+
requireValid(typeof hash === 'string' && /^[a-f0-9]{64}$/.test(hash) && !seen.has(hash) && identity(issuerId));
|
|
57
|
+
seen.add(hash);
|
|
58
|
+
requireValid(Number.isSafeInteger(createdAt) && Number.isSafeInteger(invitationExpiresAt) && Number(invitationExpiresAt) === Number(createdAt) + TTL);
|
|
59
|
+
requireValid((consumedAt === undefined && consumedBy === undefined) || (Number.isSafeInteger(consumedAt) && identity(consumedBy)));
|
|
60
|
+
}
|
|
61
|
+
return data.invitations;
|
|
62
|
+
}
|
|
63
|
+
save(invitations) {
|
|
64
|
+
mkdirSync(dirname(this.options.filePath), { recursive: true, mode: 0o700 });
|
|
65
|
+
const temporary = `${this.options.filePath}.${randomUUID()}.tmp`;
|
|
66
|
+
let fd;
|
|
67
|
+
try {
|
|
68
|
+
fd = openSync(temporary, 'wx', 0o600);
|
|
69
|
+
writeFileSync(fd, JSON.stringify({ version: 1, serviceId: this.options.serviceId, invitations }, null, 2));
|
|
70
|
+
fsyncSync(fd);
|
|
71
|
+
closeSync(fd);
|
|
72
|
+
fd = undefined;
|
|
73
|
+
renameSync(temporary, this.options.filePath);
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
if (fd !== undefined)
|
|
77
|
+
closeSync(fd);
|
|
78
|
+
try {
|
|
79
|
+
unlinkSync(temporary);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (error.code !== 'ENOENT')
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
create(issuerId, input) {
|
|
88
|
+
requireValid(identity(issuerId));
|
|
89
|
+
validateInvitation(input);
|
|
90
|
+
const now = this.now();
|
|
91
|
+
requireValid(input.expiresAt === undefined || input.expiresAt > now);
|
|
92
|
+
const current = this.read().filter(i => i.consumedAt !== undefined || i.invitationExpiresAt > now);
|
|
93
|
+
if (current.filter(i => i.consumedAt === undefined).length >= 256)
|
|
94
|
+
throw new Error('INVITATION_LIMIT');
|
|
95
|
+
// Keep bounded device-friendly metadata; retain all still-active invitations.
|
|
96
|
+
while (current.length >= 2048) {
|
|
97
|
+
const index = current.findIndex(i => i.consumedAt !== undefined);
|
|
98
|
+
if (index < 0)
|
|
99
|
+
throw new Error('INVITATION_LIMIT');
|
|
100
|
+
current.splice(index, 1);
|
|
101
|
+
}
|
|
102
|
+
const code = randomBytes(32).toString('base64url');
|
|
103
|
+
const invitation = { ...structuredClone(input), ...(input.label ? { label: input.label.trim() } : {}), hash: this.hash(code), issuerId, createdAt: now, invitationExpiresAt: now + TTL };
|
|
104
|
+
current.push(invitation);
|
|
105
|
+
this.save(current);
|
|
106
|
+
return { code, expiresAt: invitation.invitationExpiresAt };
|
|
107
|
+
}
|
|
108
|
+
consume(code, subjectId, issuerAllowed) {
|
|
109
|
+
if (!/^[A-Za-z0-9_-]{43}$/.test(code) || !identity(subjectId))
|
|
110
|
+
throw new Error('PAIRING_DENIED');
|
|
111
|
+
const current = this.read(), hash = this.hash(code), now = this.now();
|
|
112
|
+
const invite = current.find(i => i.hash === hash);
|
|
113
|
+
if (!invite || invite.consumedAt !== undefined || invite.invitationExpiresAt <= now || (invite.expiresAt !== undefined && invite.expiresAt <= now) || !issuerAllowed(invite.issuerId))
|
|
114
|
+
throw new Error('PAIRING_DENIED');
|
|
115
|
+
invite.consumedAt = now;
|
|
116
|
+
invite.consumedBy = subjectId;
|
|
117
|
+
this.save(current);
|
|
118
|
+
return { subjectId, scope: structuredClone(invite.scope), actions: [...invite.actions], ...(invite.expiresAt === undefined ? {} : { expiresAt: invite.expiresAt }), canDelegate: false };
|
|
119
|
+
}
|
|
120
|
+
subjectLabels() {
|
|
121
|
+
return Object.fromEntries(this.read().filter(i => i.consumedBy && i.label).map(i => [i.consumedBy, i.label]));
|
|
122
|
+
}
|
|
123
|
+
}
|