termdock 1.4.174 → 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 +154 -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-CdbPsjKS.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-hdxcM9yF.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-BZ454pgM.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-C185Omck.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-DNt-DTCl.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-C5Nwm_Qz.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-C5aR_7V9.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-DjV6ZNwd.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-Ct7nd4cL.js → chunk-2Q5K7J3B-BsVAXGLa.js} +1 -1
- package/dist/client/assets/{chunk-5VM5RSS4-CXem4UYP.js → chunk-5VM5RSS4-BKtILsst.js} +1 -1
- package/dist/client/assets/{chunk-F27PBJKO-DyMNEawE.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-BsW7iIrh.js → chunk-IMKFNOWR-MVtVQeTr.js} +1 -1
- package/dist/client/assets/chunk-IMKFNOWR-MVtVQeTr.js.br +0 -0
- package/dist/client/assets/{chunk-IMKFNOWR-BsW7iIrh.js.gz → chunk-IMKFNOWR-MVtVQeTr.js.gz} +0 -0
- package/dist/client/assets/{chunk-JWPE2WC7-EO8E1q1J.js → chunk-JWPE2WC7-DliIIMe7.js} +1 -1
- package/dist/client/assets/{chunk-POPQ4Y6H-0One8T9x.js → chunk-POPQ4Y6H-XBSus3IC.js} +1 -1
- package/dist/client/assets/{chunk-SVP7TREG-CDu41dkj.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-BACjgX9v.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-BJWowF6E.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-WTwHOCdm.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-D4y8TkaO.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-BzN8Ggk_.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-CwMYtt5E.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-BnTeHiAb.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-BNlh251g.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-B5FftXkQ.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-C51ZNXR2.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-CXoZH2qf.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-C7YFLlx4.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-BNQlS7-g.js → erDiagram-RLTQ6QDP-DbjtZnJx.js} +1 -1
- package/dist/client/assets/erDiagram-RLTQ6QDP-DbjtZnJx.js.br +0 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-DbjtZnJx.js.gz +0 -0
- package/dist/client/assets/{flowDiagram-HODETNUW-DIQXZ06I.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-BaJQfOqD.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-tQ4E6pwN.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-DlxiaPv1.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-DTOUeQrN.js → infoDiagram-27XIBGKW-BH4AynkM.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-5VMMS53U-8sBi-gla.js → ishikawaDiagram-5VMMS53U-BQw90AKR.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-5VMMS53U-8sBi-gla.js.br → ishikawaDiagram-5VMMS53U-BQw90AKR.js.br} +0 -0
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-BQw90AKR.js.gz +0 -0
- package/dist/client/assets/{journeyDiagram-3NMN7TZE-ZwikziWh.js → journeyDiagram-3NMN7TZE-DISkQocf.js} +1 -1
- package/dist/client/assets/journeyDiagram-3NMN7TZE-DISkQocf.js.br +0 -0
- package/dist/client/assets/{journeyDiagram-3NMN7TZE-ZwikziWh.js.gz → journeyDiagram-3NMN7TZE-DISkQocf.js.gz} +0 -0
- package/dist/client/assets/{kanban-definition-UXKFOSKX-wjuw2jQ4.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-nASMIj-E.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-pGaQeqW5.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-CKEBWbQz.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-CBjYcnwd.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-DJqvclIp.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-DLV4WPwc.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-BN7Zpsb9.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-DfHX6MUm.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-tQVSFg_k.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-C7JW1-Wh.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-BVeDiFDI.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-CBRaCpeP.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-CiLpGRuf.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-BkzVtkVY.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-DKyfGITP.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-CRgkkrgx.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-Cr7yS4CZ.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-DkPYl6E0.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 +10 -2
- package/dist/server/agent/collaborationProtocol.js +50 -0
- package/dist/server/agent/collaborationRoutes.js +86 -0
- package/dist/server/agent/collaborationStore.js +249 -21
- package/dist/server/cli.js +28 -168
- 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 +40 -72
- 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-hj8I8Cwy.js +0 -2
- package/dist/client/assets/ChangeWalkthroughPanel-hj8I8Cwy.js.br +0 -0
- package/dist/client/assets/ChangeWalkthroughPanel-hj8I8Cwy.js.gz +0 -0
- package/dist/client/assets/DagPlayground-BRoB8JX5.js +0 -1
- package/dist/client/assets/DagPlayground-BRoB8JX5.js.br +0 -0
- package/dist/client/assets/DagPlayground-BRoB8JX5.js.gz +0 -0
- package/dist/client/assets/DiffLab-CdbPsjKS.js.br +0 -0
- package/dist/client/assets/DiffLab-CdbPsjKS.js.gz +0 -0
- package/dist/client/assets/DiffReview-MQXjrRsW.js +0 -1
- package/dist/client/assets/DiffReview-MQXjrRsW.js.br +0 -0
- package/dist/client/assets/DiffReview-MQXjrRsW.js.gz +0 -0
- package/dist/client/assets/DiffReviewLab-C47CNvDM.js +0 -2
- package/dist/client/assets/DiffReviewLab-C47CNvDM.js.br +0 -0
- package/dist/client/assets/DiffReviewLab-C47CNvDM.js.gz +0 -0
- package/dist/client/assets/DiffViewer-hdxcM9yF.js.br +0 -0
- package/dist/client/assets/DiffViewer-hdxcM9yF.js.gz +0 -0
- package/dist/client/assets/ModelPreview-BZ454pgM.js.br +0 -0
- package/dist/client/assets/ModelPreview-BZ454pgM.js.gz +0 -0
- package/dist/client/assets/RightSidebar-i0Oq68YI.js +0 -55
- package/dist/client/assets/RightSidebar-i0Oq68YI.js.br +0 -0
- package/dist/client/assets/RightSidebar-i0Oq68YI.js.gz +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-C185Omck.js.br +0 -0
- package/dist/client/assets/abnfDiagram-VCTEODGH-C185Omck.js.gz +0 -0
- package/dist/client/assets/app-DAp8rVMc.js +0 -175
- package/dist/client/assets/app-DAp8rVMc.js.br +0 -0
- package/dist/client/assets/app-DAp8rVMc.js.gz +0 -0
- package/dist/client/assets/arc-DNt-DTCl.js.br +0 -0
- package/dist/client/assets/arc-DNt-DTCl.js.gz +0 -0
- package/dist/client/assets/architectureDiagram-5GKGNRK7-C5Nwm_Qz.js.br +0 -0
- package/dist/client/assets/architectureDiagram-5GKGNRK7-C5Nwm_Qz.js.gz +0 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-C5aR_7V9.js.br +0 -0
- package/dist/client/assets/blockDiagram-I7D4REHJ-C5aR_7V9.js.gz +0 -0
- package/dist/client/assets/c4Diagram-7LVT6UL2-DjV6ZNwd.js.br +0 -0
- package/dist/client/assets/c4Diagram-7LVT6UL2-DjV6ZNwd.js.gz +0 -0
- package/dist/client/assets/channel-uhLrATdy.js +0 -1
- package/dist/client/assets/chunk-F27PBJKO-DyMNEawE.js.br +0 -0
- package/dist/client/assets/chunk-F27PBJKO-DyMNEawE.js.gz +0 -0
- package/dist/client/assets/chunk-IMKFNOWR-BsW7iIrh.js.br +0 -0
- package/dist/client/assets/chunk-SVP7TREG-CDu41dkj.js.br +0 -0
- package/dist/client/assets/chunk-SVP7TREG-CDu41dkj.js.gz +0 -0
- package/dist/client/assets/chunk-TICWLB2K-BACjgX9v.js.br +0 -0
- package/dist/client/assets/chunk-TICWLB2K-BACjgX9v.js.gz +0 -0
- package/dist/client/assets/classDiagram-ZZMXUADV-CnxFfzm9.js +0 -1
- package/dist/client/assets/classDiagram-v2-VYDZK3BY-CnxFfzm9.js +0 -1
- package/dist/client/assets/cose-bilkent-JH36ORCC-WTwHOCdm.js.br +0 -0
- package/dist/client/assets/cose-bilkent-JH36ORCC-WTwHOCdm.js.gz +0 -0
- package/dist/client/assets/cynefin-OW5HDTMX-D4y8TkaO.js.br +0 -0
- package/dist/client/assets/cynefin-OW5HDTMX-D4y8TkaO.js.gz +0 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-BzN8Ggk_.js.br +0 -0
- package/dist/client/assets/cynefinDiagram-5FMLGOSQ-BzN8Ggk_.js.gz +0 -0
- package/dist/client/assets/dagre-GXQ25YYZ-CwMYtt5E.js.br +0 -0
- package/dist/client/assets/dagre-GXQ25YYZ-CwMYtt5E.js.gz +0 -0
- package/dist/client/assets/diagram-S7CK7UJ4-BnTeHiAb.js.br +0 -0
- package/dist/client/assets/diagram-S7CK7UJ4-BnTeHiAb.js.gz +0 -0
- package/dist/client/assets/diagram-UQ7AKVKN-BNlh251g.js.br +0 -0
- package/dist/client/assets/diagram-UQ7AKVKN-BNlh251g.js.gz +0 -0
- package/dist/client/assets/diagram-VSXAHHWV-B5FftXkQ.js.br +0 -0
- package/dist/client/assets/diagram-VSXAHHWV-B5FftXkQ.js.gz +0 -0
- package/dist/client/assets/diagram-VX7I27RA-C51ZNXR2.js.br +0 -0
- package/dist/client/assets/diagram-VX7I27RA-C51ZNXR2.js.gz +0 -0
- package/dist/client/assets/diagram-Z3DM3KII-CXoZH2qf.js.br +0 -0
- package/dist/client/assets/diagram-Z3DM3KII-CXoZH2qf.js.gz +0 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-C7YFLlx4.js.br +0 -0
- package/dist/client/assets/ebnfDiagram-PWID7BFC-C7YFLlx4.js.gz +0 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-BNQlS7-g.js.br +0 -0
- package/dist/client/assets/erDiagram-RLTQ6QDP-BNQlS7-g.js.gz +0 -0
- package/dist/client/assets/flowDiagram-HODETNUW-DIQXZ06I.js.br +0 -0
- package/dist/client/assets/flowDiagram-HODETNUW-DIQXZ06I.js.gz +0 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-BaJQfOqD.js.br +0 -0
- package/dist/client/assets/ganttDiagram-EL5Y4UJY-BaJQfOqD.js.gz +0 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-tQ4E6pwN.js.br +0 -0
- package/dist/client/assets/gitGraphDiagram-WWUBYQGX-tQ4E6pwN.js.gz +0 -0
- package/dist/client/assets/index-DlxiaPv1.css.br +0 -0
- package/dist/client/assets/index-DlxiaPv1.css.gz +0 -0
- package/dist/client/assets/ishikawaDiagram-5VMMS53U-8sBi-gla.js.gz +0 -0
- package/dist/client/assets/journeyDiagram-3NMN7TZE-ZwikziWh.js.br +0 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-wjuw2jQ4.js.br +0 -0
- package/dist/client/assets/kanban-definition-UXKFOSKX-wjuw2jQ4.js.gz +0 -0
- package/dist/client/assets/layout-nASMIj-E.js.br +0 -0
- package/dist/client/assets/layout-nASMIj-E.js.gz +0 -0
- package/dist/client/assets/linear-pGaQeqW5.js.br +0 -0
- package/dist/client/assets/linear-pGaQeqW5.js.gz +0 -0
- package/dist/client/assets/mermaid.core-CKEBWbQz.js.br +0 -0
- package/dist/client/assets/mermaid.core-CKEBWbQz.js.gz +0 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-CBjYcnwd.js.br +0 -0
- package/dist/client/assets/mindmap-definition-YA3MSWOX-CBjYcnwd.js.gz +0 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-DJqvclIp.js.br +0 -0
- package/dist/client/assets/pegDiagram-XKGWAZYB-DJqvclIp.js.gz +0 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-DLV4WPwc.js.br +0 -0
- package/dist/client/assets/pieDiagram-E7YTZNPT-DLV4WPwc.js.gz +0 -0
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-BN7Zpsb9.js.br +0 -0
- package/dist/client/assets/quadrantDiagram-AXDQQJYC-BN7Zpsb9.js.gz +0 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-DfHX6MUm.js.br +0 -0
- package/dist/client/assets/railroadDiagram-O6MQD6OU-DfHX6MUm.js.gz +0 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-tQVSFg_k.js.br +0 -0
- package/dist/client/assets/requirementDiagram-BXWQKSXE-tQVSFg_k.js.gz +0 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-C7JW1-Wh.js.br +0 -0
- package/dist/client/assets/sankeyDiagram-P5KCCOFB-C7JW1-Wh.js.gz +0 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-BVeDiFDI.js.br +0 -0
- package/dist/client/assets/sequenceDiagram-WJ2MYXX4-BVeDiFDI.js.gz +0 -0
- package/dist/client/assets/sizeCapture-INFHLROL-CBRaCpeP.js.br +0 -0
- package/dist/client/assets/sizeCapture-INFHLROL-CBRaCpeP.js.gz +0 -0
- package/dist/client/assets/sparkles-D0cwIvB0.js +0 -1
- package/dist/client/assets/stateDiagram-D77RDMKH-CiLpGRuf.js.br +0 -0
- package/dist/client/assets/stateDiagram-D77RDMKH-CiLpGRuf.js.gz +0 -0
- package/dist/client/assets/stateDiagram-v2-MP3YSRHH-BRbS0rv2.js +0 -1
- package/dist/client/assets/swimlanes-42K2YHIH-BkzVtkVY.js.br +0 -0
- package/dist/client/assets/swimlanes-42K2YHIH-BkzVtkVY.js.gz +0 -0
- package/dist/client/assets/swimlanesDiagram-VR7AAH4N-CeWFezkt.js +0 -8
- package/dist/client/assets/timeline-definition-24CTP7MA-DKyfGITP.js.br +0 -0
- package/dist/client/assets/timeline-definition-24CTP7MA-DKyfGITP.js.gz +0 -0
- package/dist/client/assets/vennDiagram-4TSXK5OY-CRgkkrgx.js.br +0 -0
- package/dist/client/assets/vennDiagram-4TSXK5OY-CRgkkrgx.js.gz +0 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-Cr7yS4CZ.js.br +0 -0
- package/dist/client/assets/wardleyDiagram-VM6X3IG4-Cr7yS4CZ.js.gz +0 -0
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-DkPYl6E0.js.br +0 -0
- package/dist/client/assets/xychartDiagram-S5SC5T6Z-DkPYl6E0.js.gz +0 -0
package/dist/server/entry.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
+
import { createOpenAccessRouter } from './federation/openAccess.js';
|
|
2
|
+
import { createPasswordLoginRouter } from './federation/passwordLoginRoutes.js';
|
|
1
3
|
import { apiAccessGate, isTrustedLocalRequest } from './utils/apiAccess.js';
|
|
4
|
+
import { isEncryptedRequest } from './federation/requestContext.js';
|
|
5
|
+
import { createFederationRuntime } from './federation/runtime.js';
|
|
6
|
+
import { RouteAccess } from './federation/routeAccess.js';
|
|
7
|
+
import { RouteInvitationStore } from './federation/routeInvitations.js';
|
|
8
|
+
import { attachRegisteredDirectTargets, verifyDirectTarget } from './federation/directRoutes.js';
|
|
9
|
+
import { secureChannelOriginAllowed } from './federation/originPolicy.js';
|
|
10
|
+
import { RelayRouter } from './federation/relay.js';
|
|
2
11
|
import { assertPublicSecurity, securityHeaders } from './utils/publicSecurity.js';
|
|
3
|
-
import { guardWebSocketSession } from './utils/webSocketSecurity.js';
|
|
4
|
-
import { readTerminalHandshakeDimensions } from './utils/terminalHandshakeDimensions.js';
|
|
5
12
|
import { apiCachePolicy } from './utils/apiCachePolicy.js';
|
|
6
13
|
import 'dotenv/config';
|
|
7
14
|
import express from 'express';
|
|
@@ -23,7 +30,7 @@ import { createOnboardingRouter } from './routes/onboarding.js';
|
|
|
23
30
|
import { createLocalRouter } from './routes/local.js';
|
|
24
31
|
import { csrfProtection } from './utils/csrfProtection.js';
|
|
25
32
|
import { pathValidator } from './utils/pathValidator.js';
|
|
26
|
-
import {
|
|
33
|
+
import { requireAuth, renewSessionMiddleware } from './utils/authProtection.js';
|
|
27
34
|
import { localAccessManager } from './utils/localAccess.js';
|
|
28
35
|
import { isAllowedHost, isUpgradeOriginAllowed, validateHostMiddleware, validateOriginMiddleware, } from './utils/requestSecurity.js';
|
|
29
36
|
import { getCookieSecurityOptions, setSecureCookieMode } from './utils/cookieSecurity.js';
|
|
@@ -37,22 +44,6 @@ import { getTermdockVersion, TERMDOCK_CAPABILITIES, TERMDOCK_PROTOCOL_VERSION, }
|
|
|
37
44
|
import { PORT, DEFAULT_HOST } from './config.js';
|
|
38
45
|
const CLIENT_STATE_COOKIE = 'termdock-client';
|
|
39
46
|
export const DEFAULT_PORT = PORT.backend;
|
|
40
|
-
/** Read one cookie from a raw Cookie header (upgrade requests bypass the
|
|
41
|
-
* cookie-parser middleware). Values are percent-encoded. */
|
|
42
|
-
function getCookieValue(cookieHeader, name) {
|
|
43
|
-
if (!cookieHeader)
|
|
44
|
-
return undefined;
|
|
45
|
-
const pattern = new RegExp(`(?:^|;\\s*)${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}=([^;]*)`);
|
|
46
|
-
const match = cookieHeader.match(pattern);
|
|
47
|
-
if (!match)
|
|
48
|
-
return undefined;
|
|
49
|
-
try {
|
|
50
|
-
return decodeURIComponent(match[1]);
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
return match[1];
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
47
|
const CLIENT_LOG_DEDUP_WINDOW_MS = 5_000;
|
|
57
48
|
const CLIENT_LOG_RATE_WINDOW_MS = 10_000;
|
|
58
49
|
const CLIENT_LOG_RATE_LIMIT = 120;
|
|
@@ -146,6 +137,17 @@ export function createApp(options = {}) {
|
|
|
146
137
|
app.disable('x-powered-by');
|
|
147
138
|
app.use(securityHeaders);
|
|
148
139
|
app.use(validateHostMiddleware);
|
|
140
|
+
if (options.secureRequired)
|
|
141
|
+
app.use('/api/auth/open', createOpenAccessRouter(() => app.locals.passwordRuntime?.serviceId));
|
|
142
|
+
if (options.secureRequired)
|
|
143
|
+
app.use('/api/auth/password', createPasswordLoginRouter(() => app.locals.passwordRuntime));
|
|
144
|
+
app.use('/api', (req, res, next) => {
|
|
145
|
+
if (!options.secureRequired || isEncryptedRequest(req) || isTrustedLocalRequest(req, options.localApiToken))
|
|
146
|
+
return next();
|
|
147
|
+
if (req.method === 'GET' && ['/meta', '/auth/status'].includes(req.path))
|
|
148
|
+
return next();
|
|
149
|
+
res.status(426).json({ error: 'Pair this device to use an encrypted connection', code: 'E2EE_REQUIRED' });
|
|
150
|
+
});
|
|
149
151
|
// Authenticate before allocating request bodies, including future API routes.
|
|
150
152
|
app.use(cookieParser());
|
|
151
153
|
app.use('/api', renewSessionMiddleware);
|
|
@@ -393,12 +395,78 @@ export function startServer(options = {}) {
|
|
|
393
395
|
});
|
|
394
396
|
runtimeMonitor.start();
|
|
395
397
|
const app = createApp({
|
|
398
|
+
secureRequired: true,
|
|
396
399
|
port: options.onboardingPort ?? port,
|
|
397
400
|
httpsCaPath: options.httpsCaPath,
|
|
398
401
|
localApiToken: options.localApiToken,
|
|
399
402
|
runtimeMonitor,
|
|
400
403
|
});
|
|
401
404
|
const { server, scheme } = createServerForApp(app, options);
|
|
405
|
+
let entrySubjectAllowed = (_subjectId) => false;
|
|
406
|
+
let routeInvitations;
|
|
407
|
+
const routeAccess = new RouteAccess(path.join(homedir(), '.termdock', 'federation', 'routes.json'), Date.now, (subjectId, targetServiceId) => routeInvitations?.allows(subjectId, targetServiceId) === true);
|
|
408
|
+
const relayRouter = new RelayRouter({
|
|
409
|
+
authenticate: context => context,
|
|
410
|
+
allowRegister: (principal, serviceId) => routeAccess.allowRegister(principal, serviceId),
|
|
411
|
+
allowRoute: (principal, serviceId) => routeAccess.allowRoute(principal, serviceId),
|
|
412
|
+
});
|
|
413
|
+
const dynamicDirectTargets = new Map();
|
|
414
|
+
server.once('close', () => { for (const close of dynamicDirectTargets.values())
|
|
415
|
+
close(); });
|
|
416
|
+
try {
|
|
417
|
+
const detachDirectTargets = attachRegisteredDirectTargets(relayRouter, routeAccess.configuredDirectTargets(), path.join(homedir(), '.termdock', 'federation'));
|
|
418
|
+
server.once('close', detachDirectTargets);
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
console.error('Configured direct routes unavailable; target access remains closed.');
|
|
422
|
+
}
|
|
423
|
+
server.once('close', () => relayRouter.close());
|
|
424
|
+
const federation = createFederationRuntime(app, path.join(homedir(), '.termdock', 'federation'), {
|
|
425
|
+
terminal: handleTerminalWebSocket, control: handleControlWebSocket,
|
|
426
|
+
}, {
|
|
427
|
+
listRouteAccess: () => routeInvitations?.list() || [],
|
|
428
|
+
grantRouteAccess: async (issuerId, targetServiceId, subjectId, url) => {
|
|
429
|
+
if (!routeInvitations)
|
|
430
|
+
throw new Error('ROUTE_NOT_AVAILABLE');
|
|
431
|
+
if (!routeAccess.hasConfiguredTarget(targetServiceId)) {
|
|
432
|
+
if (!url)
|
|
433
|
+
throw new Error('ROUTE_TARGET_NOT_CONFIGURED');
|
|
434
|
+
const target = { serviceId: targetServiceId, url };
|
|
435
|
+
await verifyDirectTarget(target);
|
|
436
|
+
const runtime = await federation;
|
|
437
|
+
if (!runtime.store.authorize({ subjectId: issuerId, serviceId: runtime.serviceId, action: 'authorization.manage' }).allowed)
|
|
438
|
+
throw new Error('AUTHORIZATION_DENIED');
|
|
439
|
+
routeAccess.addDirectTarget(target);
|
|
440
|
+
if (!dynamicDirectTargets.has(targetServiceId))
|
|
441
|
+
dynamicDirectTargets.set(targetServiceId, attachRegisteredDirectTargets(relayRouter, [target]));
|
|
442
|
+
}
|
|
443
|
+
return routeInvitations.grant(issuerId, targetServiceId, subjectId);
|
|
444
|
+
},
|
|
445
|
+
revokeRouteAccess: id => routeInvitations?.revoke(id) || false,
|
|
446
|
+
issueRouteTicket: (subjectId, serviceId) => routeAccess.issueRouteTicket(subjectId, serviceId),
|
|
447
|
+
hasRouteGrant: (subjectId, serviceId) => routeInvitations?.allows(subjectId, serviceId) === true,
|
|
448
|
+
createRouteInvitation: (issuerId, serviceId) => {
|
|
449
|
+
if (!routeInvitations)
|
|
450
|
+
throw new Error('ROUTE_NOT_AVAILABLE');
|
|
451
|
+
return routeInvitations.create(issuerId, serviceId);
|
|
452
|
+
},
|
|
453
|
+
consumeRouteInvitation: (code, subjectId) => {
|
|
454
|
+
if (!routeInvitations)
|
|
455
|
+
throw new Error('ROUTE_NOT_AVAILABLE');
|
|
456
|
+
return routeInvitations.consume(code, subjectId);
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
void federation.then(runtime => {
|
|
460
|
+
app.locals.passwordRuntime = runtime;
|
|
461
|
+
entrySubjectAllowed = subjectId => runtime.store.authorize({ subjectId, serviceId: runtime.serviceId, action: 'authorization.manage' }).allowed;
|
|
462
|
+
routeInvitations = new RouteInvitationStore({
|
|
463
|
+
filePath: path.join(homedir(), '.termdock', 'federation', 'route-invitations.json'), serviceId: runtime.serviceId,
|
|
464
|
+
issuerAllowed: subjectId => entrySubjectAllowed(subjectId),
|
|
465
|
+
targetAvailable: serviceId => routeAccess.hasConfiguredTarget(serviceId) && relayRouter.hasRoute(serviceId),
|
|
466
|
+
});
|
|
467
|
+
}).catch(() => console.error('Route invitations unavailable; uninitialized route grants remain denied.'));
|
|
468
|
+
void federation.catch(() => console.error('Encrypted access initialization failed; business access remains closed.'));
|
|
469
|
+
server.once('close', () => { void federation.then(runtime => runtime.close()).catch(() => { }); });
|
|
402
470
|
server.headersTimeout = 10_000;
|
|
403
471
|
server.requestTimeout = 120_000;
|
|
404
472
|
server.keepAliveTimeout = 5_000;
|
|
@@ -464,14 +532,12 @@ export function startServer(options = {}) {
|
|
|
464
532
|
zlibDeflateOptions: { level: 6, memLevel: 7 },
|
|
465
533
|
},
|
|
466
534
|
});
|
|
467
|
-
const WS_PATH_RE = /^\/api\/terminal\/([^/]+)\/ws$/;
|
|
468
|
-
const CONTROL_WS_PATH = '/api/control/ws';
|
|
469
535
|
server.on('upgrade', (request, socket, head) => {
|
|
470
536
|
if (wss.clients.size >= 256) {
|
|
471
537
|
socket.end('HTTP/1.1 503 Service Unavailable\r\nConnection: close\r\n\r\n');
|
|
472
538
|
return;
|
|
473
539
|
}
|
|
474
|
-
if (!isAllowedHost(request.headers.host)
|
|
540
|
+
if (!isAllowedHost(request.headers.host)) {
|
|
475
541
|
socket.write('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');
|
|
476
542
|
socket.destroy();
|
|
477
543
|
return;
|
|
@@ -485,41 +551,38 @@ export function startServer(options = {}) {
|
|
|
485
551
|
return;
|
|
486
552
|
}
|
|
487
553
|
const pathname = url.pathname;
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
554
|
+
if (pathname === '/api/federation/relay') {
|
|
555
|
+
// Native relay peers have no Origin. Authenticate explicitly before upgrade.
|
|
556
|
+
if (scheme !== 'https' || (request.headers.origin && !isUpgradeOriginAllowed(request.headers.origin, request.headers.host))) {
|
|
557
|
+
socket.end('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
const principal = routeAccess.authenticate(request.headers.authorization, url.searchParams.get('routeToken'));
|
|
561
|
+
if (!principal) {
|
|
562
|
+
socket.end('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
wss.handleUpgrade(request, socket, head, ws => {
|
|
566
|
+
void relayRouter.attach(ws, principal).then(attached => { if (attached)
|
|
567
|
+
ws.send(JSON.stringify({ type: 'ready' })); });
|
|
568
|
+
});
|
|
496
569
|
return;
|
|
497
570
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
571
|
+
if (pathname === '/api/federation/secure') {
|
|
572
|
+
// This exception applies only to the cookie-independent Noise handshake.
|
|
573
|
+
// Target pinning and device authorization remain mandatory inside it.
|
|
574
|
+
if (!secureChannelOriginAllowed(request.headers.origin, () => isUpgradeOriginAllowed(request.headers.origin, request.headers.host))) {
|
|
575
|
+
socket.end('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
501
578
|
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
502
|
-
|
|
503
|
-
guardWebSocketSession(ws, cookieHeader);
|
|
579
|
+
void federation.then(runtime => runtime.accept(ws, { allowOpenAccess: !!request.headers.origin && isUpgradeOriginAllowed(request.headers.origin, request.headers.host) })).catch(() => ws.close(1011, 'Encrypted access unavailable'));
|
|
504
580
|
});
|
|
505
581
|
return;
|
|
506
582
|
}
|
|
507
|
-
//
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
socket.destroy();
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
|
-
const sessionId = match[1];
|
|
514
|
-
const clientId = crypto.randomUUID();
|
|
515
|
-
const pushClientId = getCookieValue(cookieHeader, CLIENT_STATE_COOKIE);
|
|
516
|
-
// 短线重连时客户端会带上 ?since=<lastSeq>,让服务端只补发增量。
|
|
517
|
-
const sinceParam = url.searchParams.get('since');
|
|
518
|
-
const sinceSeq = sinceParam ? Math.max(0, Number.parseInt(sinceParam, 10) || 0) : 0;
|
|
519
|
-
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
520
|
-
handleTerminalWebSocket(ws, sessionId, clientId, { sinceSeq, pushClientId, streamEpoch: url.searchParams.get('epoch') ?? undefined, flowControl: url.searchParams.get('flow') === '2', independentTmux: url.searchParams.get('transport') === 'tmux-client', outputActive: url.searchParams.get('active') !== '0' }, readTerminalHandshakeDimensions(url.searchParams));
|
|
521
|
-
guardWebSocketSession(ws, cookieHeader);
|
|
522
|
-
});
|
|
583
|
+
// Business WebSockets have no plaintext fallback after pairing migration.
|
|
584
|
+
socket.end('HTTP/1.1 426 Upgrade Required\r\nConnection: close\r\n\r\n');
|
|
585
|
+
return;
|
|
523
586
|
});
|
|
524
587
|
server.listen(port, host, () => {
|
|
525
588
|
const displayHost = host === '0.0.0.0' ? 'localhost' : host;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
const METHODS = new Set(['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE']);
|
|
2
|
+
export function canonicalApiPath(path) {
|
|
3
|
+
if (typeof path !== 'string' || !path.startsWith('/api/') || /[\\\x00-\x20\x7f#]/.test(path))
|
|
4
|
+
return null;
|
|
5
|
+
const pathname = path.split('?')[0];
|
|
6
|
+
if (pathname.includes('//'))
|
|
7
|
+
return null;
|
|
8
|
+
try {
|
|
9
|
+
const segments = pathname.split('/').map((segment, index) => {
|
|
10
|
+
if (index === 0 || (segment === '' && index === pathname.split('/').length - 1))
|
|
11
|
+
return segment;
|
|
12
|
+
const value = decodeURIComponent(segment);
|
|
13
|
+
// Decode exactly once. Never allow an encoded separator, traversal segment,
|
|
14
|
+
// control byte, invalid UTF-8 or a second escape layer to change routing.
|
|
15
|
+
if (!value || value === '.' || value === '..' || /[\\/\x00-\x1f\x7f]/.test(value) || /%[a-f0-9]{2}/i.test(value))
|
|
16
|
+
throw new Error('Ambiguous API path');
|
|
17
|
+
return value;
|
|
18
|
+
});
|
|
19
|
+
const canonical = segments.join('/');
|
|
20
|
+
if (canonical.endsWith('/') && !canonical.startsWith('/api/terminal/fs/preview/'))
|
|
21
|
+
return null;
|
|
22
|
+
return canonical;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const pathOnly = canonicalApiPath;
|
|
29
|
+
/** Restricts a full-service tunnel to business APIs, excluding authentication,
|
|
30
|
+
* local-only operations and recursive federation/bootstrap endpoints. */
|
|
31
|
+
export function isBusinessApiPath(method, path) {
|
|
32
|
+
if (!METHODS.has(method))
|
|
33
|
+
return false;
|
|
34
|
+
const pathname = pathOnly(path);
|
|
35
|
+
if (!pathname)
|
|
36
|
+
return false;
|
|
37
|
+
return !/^\/api\/(?:auth|local|federation|onboarding|csrf)(?:\/|$)/i.test(pathname);
|
|
38
|
+
}
|
|
39
|
+
const fileReads = new Set(['list', 'watch', 'search', 'read', 'git-blob', 'eda-preview', 'eda-inspect', 'blob', 'video', 'download', 'diff', 'diff-files', 'untracked-files', 'git-context', 'git-bundle', 'change-audit', 'branch-diff', 'commit-diff', 'git-recent-commits', 'branch-audit', 'git-action/status', 'local-open-availability']);
|
|
40
|
+
const filePosts = new Set(['git-action', 'apply-hunk', 'upload', 'open-in-file-browser']);
|
|
41
|
+
const settings = new Set(['settings', 'toolbar-presets', 'program-detection', 'context-draft']);
|
|
42
|
+
const serviceReads = new Set(['update', 'auto-title/catalog', 'agent-hooks', 'agent-launchers', 'agent-plugins', 'quota', 'tmux/status', 'tmux/sessions', 'agent-resume-history']);
|
|
43
|
+
const reservedSessionIds = new Set(['fs', 'settings', 'client-state', 'session-inventory', 'operations', 'tmux', 'create', 'force-kill', 'serialize-state', 'agent-resume-history', 'toolbar-presets', 'context-draft', 'program-detection', 'update', 'quota', 'agent-hooks', 'agent-launchers', 'agent-plugins', 'agent-plugin-icon', 'auto-title', 'directory-suggestions']);
|
|
44
|
+
export function resolveAccessRequest(method, path) {
|
|
45
|
+
if (!isBusinessApiPath(method, path))
|
|
46
|
+
return null;
|
|
47
|
+
const pathname = pathOnly(path);
|
|
48
|
+
const read = method === 'GET' || method === 'HEAD';
|
|
49
|
+
if (/^\/api\/notifications\/(status|subscribe|preferences|unsubscribe)$/.test(pathname)) {
|
|
50
|
+
if (read && pathname.endsWith('/status'))
|
|
51
|
+
return { action: 'service.view' };
|
|
52
|
+
if (method === 'POST' && !pathname.endsWith('/status'))
|
|
53
|
+
return { action: 'service.configure' };
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const prefix = '/api/terminal/';
|
|
57
|
+
if (!pathname.startsWith(prefix))
|
|
58
|
+
return null;
|
|
59
|
+
const route = pathname.slice(prefix.length);
|
|
60
|
+
if (route.startsWith('fs/')) {
|
|
61
|
+
const op = route.slice(3);
|
|
62
|
+
if (read && (fileReads.has(op) || /^preview\/.+/.test(op)))
|
|
63
|
+
return { action: 'file.read' };
|
|
64
|
+
if ((method === 'POST' && filePosts.has(op)) || (method === 'DELETE' && ['file', 'change-audit', 'branch-audit'].includes(op)) || (method === 'GET' && op === 'cancel-slot'))
|
|
65
|
+
return { action: 'file.write' };
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
if (read && route === 'session-inventory')
|
|
69
|
+
return { action: 'service.view', metadata: 'session-inventory' };
|
|
70
|
+
if (read && route === 'client-state')
|
|
71
|
+
return { action: 'service.view', metadata: 'global-client-state' };
|
|
72
|
+
if (settings.has(route)) {
|
|
73
|
+
if (read)
|
|
74
|
+
return { action: 'service.view' };
|
|
75
|
+
if (method === 'PUT' || (method === 'DELETE' && route === 'program-detection'))
|
|
76
|
+
return { action: 'service.configure' };
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
if (read && serviceReads.has(route))
|
|
80
|
+
return { action: 'service.view' };
|
|
81
|
+
if (read && route === 'directory-suggestions')
|
|
82
|
+
return { action: 'file.read' };
|
|
83
|
+
if (method === 'POST' && route === 'create')
|
|
84
|
+
return { action: 'service.create-session' };
|
|
85
|
+
const session = /^([^/]+)(?:\/(health|attach|stream|input|resize|tmux|restart|agent-resume))?$/.exec(route);
|
|
86
|
+
if (session && !reservedSessionIds.has(session[1].toLowerCase())) {
|
|
87
|
+
const sessionId = session[1];
|
|
88
|
+
const op = session[2];
|
|
89
|
+
if (read && ['health', 'attach', 'stream', 'agent-resume'].includes(op))
|
|
90
|
+
return { action: 'session.view', sessionId };
|
|
91
|
+
if (method === 'POST' && op === 'input')
|
|
92
|
+
return { action: 'session.input', sessionId };
|
|
93
|
+
if (method === 'POST' && op === 'resize')
|
|
94
|
+
return { action: 'session.resize', sessionId };
|
|
95
|
+
if (method === 'DELETE' && !op)
|
|
96
|
+
return { action: 'session.terminate', sessionId };
|
|
97
|
+
// tmux switch-session can reach other backends; restart/agent-resume accept
|
|
98
|
+
// launch configuration. Their unrestricted bodies require service authority.
|
|
99
|
+
if (method === 'POST' && ['tmux', 'restart', 'agent-resume'].includes(op))
|
|
100
|
+
return { action: 'service.configure' };
|
|
101
|
+
}
|
|
102
|
+
// Complex operations manipulate global state or accept additional resource IDs.
|
|
103
|
+
// An unregistered operation intentionally receives no custom-grant mapping.
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
/** WS control mapping; caller must bind sessionId to the opened channel. */
|
|
107
|
+
export function terminalMessageAction(type) {
|
|
108
|
+
switch (type) {
|
|
109
|
+
case 'input':
|
|
110
|
+
case 'focus':
|
|
111
|
+
case 'agent-review-ack': return 'session.input';
|
|
112
|
+
case 'resize': return 'session.resize';
|
|
113
|
+
case 'tmux': return 'service.configure';
|
|
114
|
+
case 'ping':
|
|
115
|
+
case 'viewing':
|
|
116
|
+
case 'output-subscription':
|
|
117
|
+
case 'output-ack': return 'session.view';
|
|
118
|
+
// Legacy flow-control can pause the shared PTY; grant only to controllers.
|
|
119
|
+
case 'flow-control': return 'session.input';
|
|
120
|
+
default: return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -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
|
+
};
|