@saluzi/saluzi-edu 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +204 -195
- package/dist/guide/guide-data.json +17 -5
- package/dist/rcs-web/assets/{AdminPanel-DgLYaHiP.js → AdminPanel-Dw_WcoUm.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/ArtifactView-CY2q-o_P.js → dist/rcs-web/assets/ArtifactView-1QwTt1_U.js} +1 -1
- package/dist/rcs-web/assets/{ArtifactsGallery-CjZFQhYm.js → ArtifactsGallery-BaqQmrpp.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/Claim-CjEYu8Jg.js → dist/rcs-web/assets/Claim-DbnYUnUa.js} +1 -1
- package/dist/rcs-web/assets/{Dashboard-g2VnKR61.js → Dashboard-I2zDQu_C.js} +1 -1
- package/dist/rcs-web/assets/{Join-DiEh0bNQ.js → Join-BW_5GOxc.js} +1 -1
- package/dist/rcs-web/assets/Login-DfpBv0-E.js +1 -0
- package/dist/rcs-web/assets/{Register-CBNGPMR6.js → Register-G3MsHXWX.js} +1 -1
- package/dist/rcs-web/assets/{SessionDetail-BkGAqYDS.js → SessionDetail-C1D2E27B.js} +1 -1
- package/dist/rcs-web/assets/{SessionVisibilityBadge-DJB8gn6j.js → SessionVisibilityBadge-BKEoqr7V.js} +1 -1
- package/dist/rcs-web/assets/{Setup-BqifzvSG.js → Setup-Ba0gdFpo.js} +1 -1
- package/dist/rcs-web/assets/{ShareLanding-twz_oOTr.js → ShareLanding-CD0g9hBP.js} +1 -1
- package/dist/rcs-web/assets/{TeamCreate-BxQ0djGK.js → TeamCreate-B95jdnoW.js} +1 -1
- package/dist/rcs-web/assets/{TeamDetail-BY3nmWNo.js → TeamDetail-B1ez_Vlm.js} +1 -1
- package/dist/rcs-web/assets/{TeamList-D1uw5ZQj.js → TeamList-Cfbc_pbK.js} +1 -1
- package/dist/rcs-web/assets/{TeamSettings-DqUlB3Gr.js → TeamSettings-CM3C5h_w.js} +1 -1
- package/dist/rcs-web/assets/{UserSearchInput-HY3vDxN9.js → UserSearchInput-CXTnk0hA.js} +1 -1
- package/dist/rcs-web/assets/{UserSettings-Ca0UvlLs.js → UserSettings-Bnq6nvYc.js} +1 -1
- package/dist/rcs-web/assets/{arc-M4eZFMsF.js → arc-BSYwIwEf.js} +1 -1
- package/dist/rcs-web/assets/{architectureDiagram-3BPJPVTR-OJqqY_9Z.js → architectureDiagram-3BPJPVTR-D3RCYL_1.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/blockDiagram-GPEHLZMM-BiWsXFB4.js → dist/rcs-web/assets/blockDiagram-GPEHLZMM-CHzBQR1p.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/c4Diagram-AAUBKEIU-A47kYHom.js → dist/rcs-web/assets/c4Diagram-AAUBKEIU-BJcaDDLG.js} +1 -1
- package/dist/rcs-web/assets/channel-CbFNrUsk.js +1 -0
- package/dist/rcs-web/assets/{chunk-2J33WTMH-bR31bKjT.js → chunk-2J33WTMH-Bg0Hnyyq.js} +1 -1
- package/dist/rcs-web/assets/{chunk-4BX2VUAB-BfujiUUC.js → chunk-4BX2VUAB-DfNc0_mN.js} +1 -1
- package/dist/rcs-web/assets/{chunk-55IACEB6-CbFE9tWc.js → chunk-55IACEB6-lSLgeJjv.js} +1 -1
- package/dist/rcs-web/assets/{chunk-727SXJPM-B5uYRnS4.js → chunk-727SXJPM-CLJOi1hb.js} +1 -1
- package/dist/rcs-web/assets/{chunk-AQP2D5EJ-BpScXOxx.js → chunk-AQP2D5EJ-DTW4EE2i.js} +1 -1
- package/dist/rcs-web/assets/{chunk-FMBD7UC4-QOgv52FY.js → chunk-FMBD7UC4-B57Jsd13.js} +1 -1
- package/dist/rcs-web/assets/{chunk-ND2GUHAM-Bsk0izyp.js → chunk-ND2GUHAM-B_msF9ch.js} +1 -1
- package/dist/rcs-web/assets/{chunk-QZHKN3VN-CqUHgfxZ.js → chunk-QZHKN3VN-BKdDyhR-.js} +1 -1
- package/dist/rcs-web/assets/classDiagram-4FO5ZUOK-DjCl6Ad4.js +1 -0
- package/dist/rcs-web/assets/classDiagram-v2-Q7XG4LA2-DjCl6Ad4.js +1 -0
- package/dist/rcs-web/assets/{code-block-IT6T5CEO-BdhOgxxC.js → code-block-IT6T5CEO-By1Tu8pm.js} +1 -1
- package/dist/rcs-web/assets/{copy-BId_ACIH.js → copy-CIv8ifYN.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/cose-bilkent-S5V4N54A-CBemG5Rz.js → dist/rcs-web/assets/cose-bilkent-S5V4N54A--virsnmO.js} +1 -1
- package/dist/rcs-web/assets/{dagre-BM42HDAG-DFC7hnTe.js → dagre-BM42HDAG-DWHB77EO.js} +1 -1
- package/dist/rcs-web/assets/{diagram-2AECGRRQ--coK1eZ0.js → diagram-2AECGRRQ-Cngu5yzm.js} +1 -1
- package/dist/rcs-web/assets/{diagram-5GNKFQAL-DFv2rFe2.js → diagram-5GNKFQAL-KVNmlovi.js} +1 -1
- package/dist/rcs-web/assets/{diagram-KO2AKTUF-RJHPBcaH.js → diagram-KO2AKTUF-B7VOvyr-.js} +1 -1
- package/dist/rcs-web/assets/{diagram-LMA3HP47-C9WP4Nfe.js → diagram-LMA3HP47-BhGlWLSy.js} +1 -1
- package/dist/rcs-web/assets/{diagram-OG6HWLK6-BhsW9xiu.js → diagram-OG6HWLK6-B0nF0JCP.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/dialog-DfUuWIZS.js → dist/rcs-web/assets/dialog-D__YHb1u.js} +1 -1
- package/dist/rcs-web/assets/{erDiagram-TEJ5UH35-B5L7aiZD.js → erDiagram-TEJ5UH35-CEl50M4S.js} +1 -1
- package/dist/rcs-web/assets/{flowDiagram-I6XJVG4X-ABhMkIfD.js → flowDiagram-I6XJVG4X-fWBjdCNK.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/ganttDiagram-6RSMTGT7-DlOhmTa2.js → dist/rcs-web/assets/ganttDiagram-6RSMTGT7-BAL8_-XH.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/gitGraphDiagram-PVQCEYII-Bce8oSuR.js → dist/rcs-web/assets/gitGraphDiagram-PVQCEYII-BM_6XPU4.js} +1 -1
- package/dist/rcs-web/assets/{infoDiagram-5YYISTIA-Dkb2brY2.js → infoDiagram-5YYISTIA-DtwjUdP-.js} +1 -1
- package/dist/rcs-web/assets/{ishikawaDiagram-YF4QCWOH-ChFn0SpW.js → ishikawaDiagram-YF4QCWOH-BwxFByT9.js} +1 -1
- package/dist/rcs-web/assets/{journeyDiagram-JHISSGLW-WgDIwjKo.js → journeyDiagram-JHISSGLW-DeWYaHqa.js} +1 -1
- package/dist/rcs-web/assets/{kanban-definition-UN3LZRKU-CsOBZQC_.js → kanban-definition-UN3LZRKU-DMLiWipt.js} +1 -1
- package/dist/rcs-web/assets/{linear-D8M0DKLB.js → linear-DigNGInH.js} +1 -1
- package/dist/rcs-web/assets/{main-bilGpQlS.js → main-5C_gChTh.js} +18 -18
- package/{packages/remote-control-server/web/dist/assets/mermaid-VLURNSYL-B3Kglw26.js → dist/rcs-web/assets/mermaid-VLURNSYL-DiBP7v4h.js} +4 -4
- package/dist/rcs-web/assets/{mermaid.core-DCyqolwk.js → mermaid.core-CQwjc9NW.js} +5 -5
- package/{packages/remote-control-server/web/dist/assets/mindmap-definition-RKZ34NQL-N5OXbLbx.js → dist/rcs-web/assets/mindmap-definition-RKZ34NQL-CasTiX1-.js} +1 -1
- package/dist/rcs-web/assets/{pieDiagram-4H26LBE5-Bwldm4aG.js → pieDiagram-4H26LBE5-D94TX46C.js} +1 -1
- package/dist/rcs-web/assets/{quadrantDiagram-W4KKPZXB-ktiotrS5.js → quadrantDiagram-W4KKPZXB-wgKU_sO2.js} +1 -1
- package/dist/rcs-web/assets/{requirementDiagram-4Y6WPE33-D3G8oEBo.js → requirementDiagram-4Y6WPE33-DYKKTIT3.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/sankeyDiagram-5OEKKPKP-CKzAXLVh.js → dist/rcs-web/assets/sankeyDiagram-5OEKKPKP-BCovzmUn.js} +1 -1
- package/dist/rcs-web/assets/{sequenceDiagram-3UESZ5HK-BJk1-iXO.js → sequenceDiagram-3UESZ5HK-BLtYwGaQ.js} +1 -1
- package/dist/rcs-web/assets/{stateDiagram-AJRCARHV-DTJbbn1C.js → stateDiagram-AJRCARHV-jMZaPO--.js} +1 -1
- package/dist/rcs-web/assets/{stateDiagram-v2-BHNVJYJU-DTZV1yyE.js → stateDiagram-v2-BHNVJYJU-CoUWNtqD.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/timeline-definition-PNZ67QCA-DvSe-Rzm.js → dist/rcs-web/assets/timeline-definition-PNZ67QCA-CbqngX3Z.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/vennDiagram-CIIHVFJN-D4sxw7d3.js → dist/rcs-web/assets/vennDiagram-CIIHVFJN-F9m0_HxT.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/wardleyDiagram-YWT4CUSO-DaHUQFiA.js → dist/rcs-web/assets/wardleyDiagram-YWT4CUSO-d_Uua5Ta.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/xychartDiagram-2RQKCTM6-BS0clo8i.js → dist/rcs-web/assets/xychartDiagram-2RQKCTM6-BDwGBZT6.js} +1 -1
- package/dist/rcs-web/index.html +1 -1
- package/package.json +1 -1
- package/packages/remote-control-server/src/__tests__/helpers/db.ts +6 -0
- package/packages/remote-control-server/src/__tests__/helpers/mock-config.ts +2 -0
- package/packages/remote-control-server/src/__tests__/otp-route.test.ts +130 -0
- package/packages/remote-control-server/src/__tests__/single-user.test.ts +333 -0
- package/packages/remote-control-server/src/__tests__/web-login.test.ts +84 -0
- package/packages/remote-control-server/src/config.ts +55 -19
- package/packages/remote-control-server/src/index.ts +41 -0
- package/packages/remote-control-server/src/nodeServer.ts +143 -15
- package/packages/remote-control-server/src/registerRoutes.ts +37 -0
- package/packages/remote-control-server/src/routes/v1/environments.ts +11 -0
- package/packages/remote-control-server/src/routes/web/auth-routes.ts +21 -3
- package/packages/remote-control-server/src/services/disconnect-monitor.ts +2 -2
- package/packages/remote-control-server/src/services/single-user.ts +123 -0
- package/packages/remote-control-server/src/services/web-login.ts +96 -0
- package/packages/remote-control-server/web/dist/assets/{AdminPanel-DgLYaHiP.js → AdminPanel-Dw_WcoUm.js} +1 -1
- package/{dist/rcs-web/assets/ArtifactView-CY2q-o_P.js → packages/remote-control-server/web/dist/assets/ArtifactView-1QwTt1_U.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{ArtifactsGallery-CjZFQhYm.js → ArtifactsGallery-BaqQmrpp.js} +1 -1
- package/{dist/rcs-web/assets/Claim-CjEYu8Jg.js → packages/remote-control-server/web/dist/assets/Claim-DbnYUnUa.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{Dashboard-g2VnKR61.js → Dashboard-I2zDQu_C.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{Join-DiEh0bNQ.js → Join-BW_5GOxc.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/Login-DfpBv0-E.js +1 -0
- package/packages/remote-control-server/web/dist/assets/{Register-CBNGPMR6.js → Register-G3MsHXWX.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{SessionDetail-BkGAqYDS.js → SessionDetail-C1D2E27B.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{SessionVisibilityBadge-DJB8gn6j.js → SessionVisibilityBadge-BKEoqr7V.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{Setup-BqifzvSG.js → Setup-Ba0gdFpo.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{ShareLanding-twz_oOTr.js → ShareLanding-CD0g9hBP.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{TeamCreate-BxQ0djGK.js → TeamCreate-B95jdnoW.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{TeamDetail-BY3nmWNo.js → TeamDetail-B1ez_Vlm.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{TeamList-D1uw5ZQj.js → TeamList-Cfbc_pbK.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{TeamSettings-DqUlB3Gr.js → TeamSettings-CM3C5h_w.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{UserSearchInput-HY3vDxN9.js → UserSearchInput-CXTnk0hA.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{UserSettings-Ca0UvlLs.js → UserSettings-Bnq6nvYc.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{arc-M4eZFMsF.js → arc-BSYwIwEf.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{architectureDiagram-3BPJPVTR-OJqqY_9Z.js → architectureDiagram-3BPJPVTR-D3RCYL_1.js} +1 -1
- package/{dist/rcs-web/assets/blockDiagram-GPEHLZMM-BiWsXFB4.js → packages/remote-control-server/web/dist/assets/blockDiagram-GPEHLZMM-CHzBQR1p.js} +1 -1
- package/{dist/rcs-web/assets/c4Diagram-AAUBKEIU-A47kYHom.js → packages/remote-control-server/web/dist/assets/c4Diagram-AAUBKEIU-BJcaDDLG.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/channel-CbFNrUsk.js +1 -0
- package/packages/remote-control-server/web/dist/assets/{chunk-2J33WTMH-bR31bKjT.js → chunk-2J33WTMH-Bg0Hnyyq.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-4BX2VUAB-BfujiUUC.js → chunk-4BX2VUAB-DfNc0_mN.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-55IACEB6-CbFE9tWc.js → chunk-55IACEB6-lSLgeJjv.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-727SXJPM-B5uYRnS4.js → chunk-727SXJPM-CLJOi1hb.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-AQP2D5EJ-BpScXOxx.js → chunk-AQP2D5EJ-DTW4EE2i.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-FMBD7UC4-QOgv52FY.js → chunk-FMBD7UC4-B57Jsd13.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-ND2GUHAM-Bsk0izyp.js → chunk-ND2GUHAM-B_msF9ch.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-QZHKN3VN-CqUHgfxZ.js → chunk-QZHKN3VN-BKdDyhR-.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/classDiagram-4FO5ZUOK-DjCl6Ad4.js +1 -0
- package/packages/remote-control-server/web/dist/assets/classDiagram-v2-Q7XG4LA2-DjCl6Ad4.js +1 -0
- package/packages/remote-control-server/web/dist/assets/{code-block-IT6T5CEO-BdhOgxxC.js → code-block-IT6T5CEO-By1Tu8pm.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{copy-BId_ACIH.js → copy-CIv8ifYN.js} +1 -1
- package/{dist/rcs-web/assets/cose-bilkent-S5V4N54A-CBemG5Rz.js → packages/remote-control-server/web/dist/assets/cose-bilkent-S5V4N54A--virsnmO.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{dagre-BM42HDAG-DFC7hnTe.js → dagre-BM42HDAG-DWHB77EO.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{diagram-2AECGRRQ--coK1eZ0.js → diagram-2AECGRRQ-Cngu5yzm.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{diagram-5GNKFQAL-DFv2rFe2.js → diagram-5GNKFQAL-KVNmlovi.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{diagram-KO2AKTUF-RJHPBcaH.js → diagram-KO2AKTUF-B7VOvyr-.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{diagram-LMA3HP47-C9WP4Nfe.js → diagram-LMA3HP47-BhGlWLSy.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{diagram-OG6HWLK6-BhsW9xiu.js → diagram-OG6HWLK6-B0nF0JCP.js} +1 -1
- package/{dist/rcs-web/assets/dialog-DfUuWIZS.js → packages/remote-control-server/web/dist/assets/dialog-D__YHb1u.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{erDiagram-TEJ5UH35-B5L7aiZD.js → erDiagram-TEJ5UH35-CEl50M4S.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{flowDiagram-I6XJVG4X-ABhMkIfD.js → flowDiagram-I6XJVG4X-fWBjdCNK.js} +1 -1
- package/{dist/rcs-web/assets/ganttDiagram-6RSMTGT7-DlOhmTa2.js → packages/remote-control-server/web/dist/assets/ganttDiagram-6RSMTGT7-BAL8_-XH.js} +1 -1
- package/{dist/rcs-web/assets/gitGraphDiagram-PVQCEYII-Bce8oSuR.js → packages/remote-control-server/web/dist/assets/gitGraphDiagram-PVQCEYII-BM_6XPU4.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{infoDiagram-5YYISTIA-Dkb2brY2.js → infoDiagram-5YYISTIA-DtwjUdP-.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{ishikawaDiagram-YF4QCWOH-ChFn0SpW.js → ishikawaDiagram-YF4QCWOH-BwxFByT9.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{journeyDiagram-JHISSGLW-WgDIwjKo.js → journeyDiagram-JHISSGLW-DeWYaHqa.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{kanban-definition-UN3LZRKU-CsOBZQC_.js → kanban-definition-UN3LZRKU-DMLiWipt.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{linear-D8M0DKLB.js → linear-DigNGInH.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{main-bilGpQlS.js → main-5C_gChTh.js} +18 -18
- package/{dist/rcs-web/assets/mermaid-VLURNSYL-B3Kglw26.js → packages/remote-control-server/web/dist/assets/mermaid-VLURNSYL-DiBP7v4h.js} +4 -4
- package/packages/remote-control-server/web/dist/assets/{mermaid.core-DCyqolwk.js → mermaid.core-CQwjc9NW.js} +5 -5
- package/{dist/rcs-web/assets/mindmap-definition-RKZ34NQL-N5OXbLbx.js → packages/remote-control-server/web/dist/assets/mindmap-definition-RKZ34NQL-CasTiX1-.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{pieDiagram-4H26LBE5-Bwldm4aG.js → pieDiagram-4H26LBE5-D94TX46C.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{quadrantDiagram-W4KKPZXB-ktiotrS5.js → quadrantDiagram-W4KKPZXB-wgKU_sO2.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{requirementDiagram-4Y6WPE33-D3G8oEBo.js → requirementDiagram-4Y6WPE33-DYKKTIT3.js} +1 -1
- package/{dist/rcs-web/assets/sankeyDiagram-5OEKKPKP-CKzAXLVh.js → packages/remote-control-server/web/dist/assets/sankeyDiagram-5OEKKPKP-BCovzmUn.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{sequenceDiagram-3UESZ5HK-BJk1-iXO.js → sequenceDiagram-3UESZ5HK-BLtYwGaQ.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{stateDiagram-AJRCARHV-DTJbbn1C.js → stateDiagram-AJRCARHV-jMZaPO--.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{stateDiagram-v2-BHNVJYJU-DTZV1yyE.js → stateDiagram-v2-BHNVJYJU-CoUWNtqD.js} +1 -1
- package/{dist/rcs-web/assets/timeline-definition-PNZ67QCA-DvSe-Rzm.js → packages/remote-control-server/web/dist/assets/timeline-definition-PNZ67QCA-CbqngX3Z.js} +1 -1
- package/{dist/rcs-web/assets/vennDiagram-CIIHVFJN-D4sxw7d3.js → packages/remote-control-server/web/dist/assets/vennDiagram-CIIHVFJN-F9m0_HxT.js} +1 -1
- package/{dist/rcs-web/assets/wardleyDiagram-YWT4CUSO-DaHUQFiA.js → packages/remote-control-server/web/dist/assets/wardleyDiagram-YWT4CUSO-d_Uua5Ta.js} +1 -1
- package/{dist/rcs-web/assets/xychartDiagram-2RQKCTM6-BS0clo8i.js → packages/remote-control-server/web/dist/assets/xychartDiagram-2RQKCTM6-BDwGBZT6.js} +1 -1
- package/packages/remote-control-server/web/dist/index.html +1 -1
- package/dist/rcs-web/assets/Login-CvYL4i61.js +0 -1
- package/dist/rcs-web/assets/channel-CpK1-PDN.js +0 -1
- package/dist/rcs-web/assets/classDiagram-4FO5ZUOK-BIsjdqJz.js +0 -1
- package/dist/rcs-web/assets/classDiagram-v2-Q7XG4LA2-BIsjdqJz.js +0 -1
- package/packages/remote-control-server/web/dist/assets/Login-CvYL4i61.js +0 -1
- package/packages/remote-control-server/web/dist/assets/channel-CpK1-PDN.js +0 -1
- package/packages/remote-control-server/web/dist/assets/classDiagram-4FO5ZUOK-BIsjdqJz.js +0 -1
- package/packages/remote-control-server/web/dist/assets/classDiagram-v2-Q7XG4LA2-BIsjdqJz.js +0 -1
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, mock } from 'bun:test'
|
|
2
|
+
import { createMockConfig } from './helpers/mock-config'
|
|
3
|
+
|
|
4
|
+
// Mutable config mock — individual describes flip `singleUser` (and the
|
|
5
|
+
// derived `allowRegistration`) to exercise both modes against the same
|
|
6
|
+
// route modules.
|
|
7
|
+
const configState = createMockConfig()
|
|
8
|
+
mock.module('../config', () => ({
|
|
9
|
+
config: configState,
|
|
10
|
+
getBaseUrl: () => configState.baseUrl,
|
|
11
|
+
}))
|
|
12
|
+
|
|
13
|
+
import { Hono } from 'hono'
|
|
14
|
+
import type { Database } from 'bun:sqlite'
|
|
15
|
+
import { randomUUID } from 'node:crypto'
|
|
16
|
+
import { tmpdir } from 'node:os'
|
|
17
|
+
import { resolve } from 'node:path'
|
|
18
|
+
import v1Environments from '../routes/v1/environments'
|
|
19
|
+
import webAuthRoutes from '../routes/web/auth-routes'
|
|
20
|
+
import { storeReset } from '../store'
|
|
21
|
+
import { initDatabase, resetDbSingleton, getDb } from '../db/sqlite'
|
|
22
|
+
import {
|
|
23
|
+
ensureSingleUserAdmin,
|
|
24
|
+
getSingleUserAdminUuid,
|
|
25
|
+
SINGLE_USER_DEFAULT_PASSWORD,
|
|
26
|
+
SINGLE_USER_USERNAME,
|
|
27
|
+
} from '../services/single-user'
|
|
28
|
+
import { createUserDirectly } from './helpers/fixtures'
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Single-user mode (`slz rcs one`):
|
|
32
|
+
* - First deployment auto-initializes the sole admin account
|
|
33
|
+
* (admin / admin123) — login with those credentials works immediately.
|
|
34
|
+
* - Any worker binding is auto-owned by that account: no X-User-Id, no
|
|
35
|
+
* X-Username, no claim_token in the register response.
|
|
36
|
+
* - Self-registration and invitation joins are refused; register-status
|
|
37
|
+
* reports closed registration so the Web UI hides the sign-up link.
|
|
38
|
+
* - Full mode (singleUser=false) keeps the claim flow.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
function freshDb(): Database {
|
|
42
|
+
try {
|
|
43
|
+
resetDbSingleton()
|
|
44
|
+
} catch {
|
|
45
|
+
// ignore cleanup errors
|
|
46
|
+
}
|
|
47
|
+
const path = resolve(
|
|
48
|
+
tmpdir(),
|
|
49
|
+
`rcs-single-user-${randomUUID().replace(/-/g, '').slice(0, 8)}.db`,
|
|
50
|
+
)
|
|
51
|
+
return initDatabase(path)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const ADMIN_KEY = {
|
|
55
|
+
Authorization: 'Bearer test-api-key',
|
|
56
|
+
'Content-Type': 'application/json',
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function createBridgeApp(): Hono {
|
|
60
|
+
const app = new Hono()
|
|
61
|
+
app.route('/v1/environments', v1Environments)
|
|
62
|
+
return app
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function createAuthApp(): Hono {
|
|
66
|
+
const app = new Hono()
|
|
67
|
+
app.route('/web/auth', webAuthRoutes)
|
|
68
|
+
return app
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe('ensureSingleUserAdmin', () => {
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
freshDb()
|
|
74
|
+
storeReset()
|
|
75
|
+
configState.singleUser = true
|
|
76
|
+
configState.allowRegistration = false
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('creates admin/admin123 on an empty users table (first deployment)', async () => {
|
|
80
|
+
const admin = await ensureSingleUserAdmin()
|
|
81
|
+
expect(admin).not.toBeNull()
|
|
82
|
+
expect(admin!.created).toBe(true)
|
|
83
|
+
expect(admin!.username).toBe(SINGLE_USER_USERNAME)
|
|
84
|
+
|
|
85
|
+
const row = getDb().query('SELECT id, username, role FROM users').get() as {
|
|
86
|
+
id: string
|
|
87
|
+
username: string
|
|
88
|
+
role: string
|
|
89
|
+
}
|
|
90
|
+
expect(row.id).toBe(admin!.userId)
|
|
91
|
+
expect(row.username).toBe(SINGLE_USER_USERNAME)
|
|
92
|
+
expect(row.role).toBe('admin')
|
|
93
|
+
|
|
94
|
+
// The default credential verifies against the stored argon2id hash.
|
|
95
|
+
const { verifyPassword } = await import('../auth/password')
|
|
96
|
+
const hashRow = getDb()
|
|
97
|
+
.query('SELECT password_hash FROM users WHERE username = $u')
|
|
98
|
+
.get({ $u: SINGLE_USER_USERNAME }) as { password_hash: string }
|
|
99
|
+
expect(
|
|
100
|
+
verifyPassword(SINGLE_USER_DEFAULT_PASSWORD, hashRow.password_hash),
|
|
101
|
+
).toBe(true)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('is idempotent — second call reuses the account, no duplicates', async () => {
|
|
105
|
+
const first = await ensureSingleUserAdmin()
|
|
106
|
+
const second = await ensureSingleUserAdmin()
|
|
107
|
+
expect(second!.created).toBe(false)
|
|
108
|
+
expect(second!.userId).toBe(first!.userId)
|
|
109
|
+
|
|
110
|
+
const count = getDb()
|
|
111
|
+
.query('SELECT COUNT(*) as count FROM users')
|
|
112
|
+
.get() as { count: number }
|
|
113
|
+
expect(count.count).toBe(1)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('reuses an existing admin instead of creating a second account', async () => {
|
|
117
|
+
createUserDirectly(getDb(), { username: 'alice', role: 'admin' })
|
|
118
|
+
const admin = await ensureSingleUserAdmin()
|
|
119
|
+
expect(admin!.created).toBe(false)
|
|
120
|
+
expect(admin!.username).toBe('alice')
|
|
121
|
+
|
|
122
|
+
const count = getDb()
|
|
123
|
+
.query('SELECT COUNT(*) as count FROM users')
|
|
124
|
+
.get() as { count: number }
|
|
125
|
+
expect(count.count).toBe(1)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test('returns null when users exist but none is an admin', async () => {
|
|
129
|
+
createUserDirectly(getDb(), { username: 'bob', role: 'member' })
|
|
130
|
+
const admin = await ensureSingleUserAdmin()
|
|
131
|
+
expect(admin).toBeNull()
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('getSingleUserAdminUuid resolves the admin id', async () => {
|
|
135
|
+
expect(getSingleUserAdminUuid()).toBeNull() // empty DB
|
|
136
|
+
const admin = await ensureSingleUserAdmin()
|
|
137
|
+
expect(getSingleUserAdminUuid()).toBe(admin!.userId)
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
describe('POST /v1/environments/bridge — single-user auto-ownership', () => {
|
|
142
|
+
let app: Hono
|
|
143
|
+
let db: Database
|
|
144
|
+
|
|
145
|
+
beforeEach(() => {
|
|
146
|
+
db = freshDb()
|
|
147
|
+
storeReset()
|
|
148
|
+
app = createBridgeApp()
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test('bare API key registration is owned by the single admin, no claim', async () => {
|
|
152
|
+
configState.singleUser = true
|
|
153
|
+
const admin = await ensureSingleUserAdmin()
|
|
154
|
+
expect(admin).not.toBeNull()
|
|
155
|
+
|
|
156
|
+
const res = await app.request('/v1/environments/bridge', {
|
|
157
|
+
method: 'POST',
|
|
158
|
+
headers: ADMIN_KEY,
|
|
159
|
+
body: JSON.stringify({ machine_name: 'local-worker' }),
|
|
160
|
+
})
|
|
161
|
+
expect(res.status).toBe(200)
|
|
162
|
+
const body = (await res.json()) as {
|
|
163
|
+
environment_id: string
|
|
164
|
+
claim_token?: string
|
|
165
|
+
claim_url?: string
|
|
166
|
+
}
|
|
167
|
+
expect(body.environment_id).toMatch(/^env_/)
|
|
168
|
+
// No claim flow in single-user mode — the env already has an owner.
|
|
169
|
+
expect(body.claim_token).toBeUndefined()
|
|
170
|
+
expect(body.claim_url).toBeUndefined()
|
|
171
|
+
|
|
172
|
+
const row = db
|
|
173
|
+
.query('SELECT owner_user_id, claim_token FROM environments')
|
|
174
|
+
.get() as { owner_user_id: string; claim_token: string | null }
|
|
175
|
+
expect(row.owner_user_id).toBe(admin!.userId)
|
|
176
|
+
expect(row.claim_token).toBeNull()
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test('reattach keeps the env owned by the single admin', async () => {
|
|
180
|
+
configState.singleUser = true
|
|
181
|
+
const admin = await ensureSingleUserAdmin()
|
|
182
|
+
|
|
183
|
+
const first = await app.request('/v1/environments/bridge', {
|
|
184
|
+
method: 'POST',
|
|
185
|
+
headers: ADMIN_KEY,
|
|
186
|
+
body: JSON.stringify({ machine_name: 'w1' }),
|
|
187
|
+
})
|
|
188
|
+
const { environment_id } = (await first.json()) as {
|
|
189
|
+
environment_id: string
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Restart with the bridge pointer — reattach must not fork or un-own.
|
|
193
|
+
const second = await app.request('/v1/environments/bridge', {
|
|
194
|
+
method: 'POST',
|
|
195
|
+
headers: ADMIN_KEY,
|
|
196
|
+
body: JSON.stringify({ environment_id, machine_name: 'w1' }),
|
|
197
|
+
})
|
|
198
|
+
expect(second.status).toBe(200)
|
|
199
|
+
const count = db
|
|
200
|
+
.query('SELECT COUNT(*) as count FROM environments')
|
|
201
|
+
.get() as { count: number }
|
|
202
|
+
expect(count.count).toBe(1)
|
|
203
|
+
const row = db.query('SELECT owner_user_id FROM environments').get() as {
|
|
204
|
+
owner_user_id: string
|
|
205
|
+
}
|
|
206
|
+
expect(row.owner_user_id).toBe(admin!.userId)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
test('ACP worker session is bound to the single admin', async () => {
|
|
210
|
+
configState.singleUser = true
|
|
211
|
+
const admin = await ensureSingleUserAdmin()
|
|
212
|
+
|
|
213
|
+
const res = await app.request('/v1/environments/bridge', {
|
|
214
|
+
method: 'POST',
|
|
215
|
+
headers: ADMIN_KEY,
|
|
216
|
+
body: JSON.stringify({
|
|
217
|
+
machine_name: 'acp-agent',
|
|
218
|
+
worker_type: 'acp',
|
|
219
|
+
bridge_id: 'grp_local',
|
|
220
|
+
}),
|
|
221
|
+
})
|
|
222
|
+
expect(res.status).toBe(200)
|
|
223
|
+
const { session_id } = (await res.json()) as { session_id: string }
|
|
224
|
+
expect(session_id).toBeDefined()
|
|
225
|
+
|
|
226
|
+
const owner = db
|
|
227
|
+
.query(
|
|
228
|
+
`SELECT owner_id FROM session_owners
|
|
229
|
+
WHERE session_id = $s AND owner_type = 'user'`,
|
|
230
|
+
)
|
|
231
|
+
.get({ $s: session_id }) as { owner_id: string }
|
|
232
|
+
expect(owner.owner_id).toBe(admin!.userId)
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
test('full mode (singleUser=false) keeps the claim flow', async () => {
|
|
236
|
+
configState.singleUser = false
|
|
237
|
+
const res = await app.request('/v1/environments/bridge', {
|
|
238
|
+
method: 'POST',
|
|
239
|
+
headers: ADMIN_KEY,
|
|
240
|
+
body: JSON.stringify({ machine_name: 'w1' }),
|
|
241
|
+
})
|
|
242
|
+
expect(res.status).toBe(200)
|
|
243
|
+
const body = (await res.json()) as { claim_token: string }
|
|
244
|
+
expect(body.claim_token).toMatch(/^clm_/)
|
|
245
|
+
|
|
246
|
+
const row = db.query('SELECT owner_user_id FROM environments').get() as {
|
|
247
|
+
owner_user_id: string | null
|
|
248
|
+
}
|
|
249
|
+
expect(row.owner_user_id).toBeNull()
|
|
250
|
+
})
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
describe('web auth routes — single-user mode', () => {
|
|
254
|
+
let app: Hono
|
|
255
|
+
|
|
256
|
+
beforeEach(() => {
|
|
257
|
+
freshDb()
|
|
258
|
+
storeReset()
|
|
259
|
+
app = createAuthApp()
|
|
260
|
+
configState.singleUser = true
|
|
261
|
+
configState.allowRegistration = false
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
test('first deployment: bootstrap → login with admin/admin123 works', async () => {
|
|
265
|
+
const admin = await ensureSingleUserAdmin()
|
|
266
|
+
expect(admin!.created).toBe(true)
|
|
267
|
+
|
|
268
|
+
const res = await app.request('/web/auth/login', {
|
|
269
|
+
method: 'POST',
|
|
270
|
+
headers: { 'Content-Type': 'application/json' },
|
|
271
|
+
body: JSON.stringify({
|
|
272
|
+
username: SINGLE_USER_USERNAME,
|
|
273
|
+
password: SINGLE_USER_DEFAULT_PASSWORD,
|
|
274
|
+
}),
|
|
275
|
+
})
|
|
276
|
+
expect(res.status).toBe(200)
|
|
277
|
+
const body = (await res.json()) as { accessToken: string }
|
|
278
|
+
expect(body.accessToken).toBeDefined()
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
test('register-status reports closed registration', async () => {
|
|
282
|
+
const res = await app.request('/web/auth/register-status')
|
|
283
|
+
expect(res.status).toBe(200)
|
|
284
|
+
const body = (await res.json()) as { allowed: boolean }
|
|
285
|
+
expect(body.allowed).toBe(false)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
test('self-registration is refused', async () => {
|
|
289
|
+
const res = await app.request('/web/auth/register', {
|
|
290
|
+
method: 'POST',
|
|
291
|
+
headers: { 'Content-Type': 'application/json' },
|
|
292
|
+
body: JSON.stringify({ username: 'intruder', password: 'password123' }),
|
|
293
|
+
})
|
|
294
|
+
expect(res.status).toBe(403)
|
|
295
|
+
const count = getDb()
|
|
296
|
+
.query('SELECT COUNT(*) as count FROM users')
|
|
297
|
+
.get() as { count: number }
|
|
298
|
+
expect(count.count).toBe(0)
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
test('invitation join is refused', async () => {
|
|
302
|
+
const res = await app.request('/web/auth/join', {
|
|
303
|
+
method: 'POST',
|
|
304
|
+
headers: { 'Content-Type': 'application/json' },
|
|
305
|
+
body: JSON.stringify({
|
|
306
|
+
inviteToken: 'inv_whatever',
|
|
307
|
+
username: 'guest',
|
|
308
|
+
password: 'password123',
|
|
309
|
+
}),
|
|
310
|
+
})
|
|
311
|
+
expect(res.status).toBe(403)
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
test('setup-status reports no setup needed and the single-user mode', async () => {
|
|
315
|
+
await ensureSingleUserAdmin()
|
|
316
|
+
const res = await app.request('/web/auth/setup-status')
|
|
317
|
+
expect(res.status).toBe(200)
|
|
318
|
+
const body = (await res.json()) as {
|
|
319
|
+
needsSetup: boolean
|
|
320
|
+
singleUser: boolean
|
|
321
|
+
}
|
|
322
|
+
expect(body.needsSetup).toBe(false)
|
|
323
|
+
expect(body.singleUser).toBe(true)
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
test('full mode keeps registration open (contrast)', async () => {
|
|
327
|
+
configState.singleUser = false
|
|
328
|
+
configState.allowRegistration = true
|
|
329
|
+
const res = await app.request('/web/auth/register-status')
|
|
330
|
+
const body = (await res.json()) as { allowed: boolean }
|
|
331
|
+
expect(body.allowed).toBe(true)
|
|
332
|
+
})
|
|
333
|
+
})
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
MAX_PENDING_CODES,
|
|
4
|
+
consumeWebLoginCode,
|
|
5
|
+
issueWebLoginCode,
|
|
6
|
+
_resetWebLoginCodes,
|
|
7
|
+
} from '../services/web-login'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* One-time web login codes (`slz rcs one` browser auto-open):
|
|
11
|
+
* - single use — the first consume returns the record, the second null
|
|
12
|
+
* - 2-minute TTL — expired codes are rejected and pruned
|
|
13
|
+
* - target paths are clamped to same-origin paths (no '//host' open redirect)
|
|
14
|
+
* - the pending map is bounded
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
describe('web-login codes', () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
_resetWebLoginCodes()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('consume returns the record exactly once (single use)', () => {
|
|
23
|
+
const issued = issueWebLoginCode('usr_abc', '/code/session_1')
|
|
24
|
+
expect(issued.code).toMatch(/^wlc_[0-9a-f]{32}$/)
|
|
25
|
+
expect(issued.targetPath).toBe('/code/session_1')
|
|
26
|
+
expect(issued.userId).toBe('usr_abc')
|
|
27
|
+
|
|
28
|
+
const consumed = consumeWebLoginCode(issued.code)
|
|
29
|
+
expect(consumed).not.toBeNull()
|
|
30
|
+
expect(consumed?.userId).toBe('usr_abc')
|
|
31
|
+
expect(consumed?.targetPath).toBe('/code/session_1')
|
|
32
|
+
|
|
33
|
+
// Second consume — already used.
|
|
34
|
+
expect(consumeWebLoginCode(issued.code)).toBeNull()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('unknown code returns null', () => {
|
|
38
|
+
expect(
|
|
39
|
+
consumeWebLoginCode('wlc_deadbeefdeadbeefdeadbeefdeadbeef'),
|
|
40
|
+
).toBeNull()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('targetPath is clamped to a same-origin path', () => {
|
|
44
|
+
expect(issueWebLoginCode('usr_abc', '/code/session_1').targetPath).toBe(
|
|
45
|
+
'/code/session_1',
|
|
46
|
+
)
|
|
47
|
+
// No leading slash → root.
|
|
48
|
+
expect(issueWebLoginCode('usr_abc', 'code/session_1').targetPath).toBe('/')
|
|
49
|
+
// Protocol-relative would be an open redirect — clamp.
|
|
50
|
+
expect(issueWebLoginCode('usr_abc', '//evil.example/x').targetPath).toBe(
|
|
51
|
+
'/',
|
|
52
|
+
)
|
|
53
|
+
// Absolute URL → root.
|
|
54
|
+
expect(
|
|
55
|
+
issueWebLoginCode('usr_abc', 'https://evil.example/x').targetPath,
|
|
56
|
+
).toBe('/')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('expired code is rejected', () => {
|
|
60
|
+
const issued = issueWebLoginCode('usr_abc', '/code/')
|
|
61
|
+
const realNow = Date.now
|
|
62
|
+
// Jump past the 2-minute TTL.
|
|
63
|
+
Date.now = () => realNow() + 3 * 60 * 1000
|
|
64
|
+
try {
|
|
65
|
+
expect(consumeWebLoginCode(issued.code)).toBeNull()
|
|
66
|
+
} finally {
|
|
67
|
+
Date.now = realNow
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('pending map is bounded — oldest entries are evicted', () => {
|
|
72
|
+
const oldest = issueWebLoginCode('usr_abc', '/code/oldest')
|
|
73
|
+
// Fill to exactly the cap without exceeding it.
|
|
74
|
+
let last: ReturnType<typeof issueWebLoginCode> | undefined
|
|
75
|
+
for (let i = 1; i < MAX_PENDING_CODES; i++) {
|
|
76
|
+
last = issueWebLoginCode('usr_abc', `/code/fill-${i}`)
|
|
77
|
+
}
|
|
78
|
+
// One more beyond the cap — the oldest was evicted to make room…
|
|
79
|
+
issueWebLoginCode('usr_abc', '/code/overflow')
|
|
80
|
+
expect(consumeWebLoginCode(oldest.code)).toBeNull()
|
|
81
|
+
// …while recent entries stay intact.
|
|
82
|
+
expect(consumeWebLoginCode(last!.code)).not.toBeNull()
|
|
83
|
+
})
|
|
84
|
+
})
|
|
@@ -5,7 +5,49 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export const DEFAULT_RCS_API_KEY = 'sk-slz'
|
|
7
7
|
|
|
8
|
-
export
|
|
8
|
+
export interface RcsConfig {
|
|
9
|
+
version: string
|
|
10
|
+
port: number
|
|
11
|
+
host: string
|
|
12
|
+
apiKeys: string[]
|
|
13
|
+
baseUrl: string
|
|
14
|
+
pollTimeout: number
|
|
15
|
+
heartbeatInterval: number
|
|
16
|
+
jwtExpiresIn: number
|
|
17
|
+
disconnectTimeout: number
|
|
18
|
+
webCorsOrigins: string[]
|
|
19
|
+
/** Whether the web UI allows open user self-registration (POST /web/auth/register).
|
|
20
|
+
* When false, only admin invitation (/join) can create new users. Default true.
|
|
21
|
+
* Forced false in single-user mode — exactly one account exists. */
|
|
22
|
+
allowRegistration: boolean
|
|
23
|
+
/** Single-user mode (`slz rcs one`): exactly one admin account
|
|
24
|
+
* (auto-created admin/admin123 on first deployment), every worker
|
|
25
|
+
* auto-owned by it, registration/join disabled, host defaults to
|
|
26
|
+
* 127.0.0.1. Local deployments only — convenience over auth.
|
|
27
|
+
* See services/single-user.ts. */
|
|
28
|
+
singleUser: boolean
|
|
29
|
+
/** Bun WebSocket idle timeout (seconds). Bun sends protocol-level pings after
|
|
30
|
+
* this many seconds of no received data. Must be shorter than any reverse
|
|
31
|
+
* proxy's idle timeout (nginx default 60s, Cloudflare 100s). Default 30s. */
|
|
32
|
+
wsIdleTimeout: number
|
|
33
|
+
/** Server→client keep_alive data-frame interval (seconds). Keeps reverse
|
|
34
|
+
* proxies from closing idle connections. Default 20s. */
|
|
35
|
+
wsKeepaliveInterval: number
|
|
36
|
+
/** Server-side client-inactivity timeout (seconds). If the bridge child
|
|
37
|
+
* process sends no data frame within this window, the server closes the
|
|
38
|
+
* WebSocket as dead. Must exceed the child's keep_alive interval
|
|
39
|
+
* (session_keepalive_interval_v2_ms, default 120s) plus jitter, otherwise
|
|
40
|
+
* healthy idle sessions get torn down between keepalives. Default 300s. */
|
|
41
|
+
wsClientInactivityTimeout: number
|
|
42
|
+
/** Whether artifact content (GET /v1/artifacts/:id/content) is served
|
|
43
|
+
* without authentication. Default true — the unguessable artifact id is
|
|
44
|
+
* the secret, same trust model as the upstream cloud-artifacts service.
|
|
45
|
+
* Set RCS_ARTIFACTS_PUBLIC_ACCESS=false to gate reads behind web session
|
|
46
|
+
* or worker credentials. */
|
|
47
|
+
artifactsPublicAccess: boolean
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const config: RcsConfig = {
|
|
9
51
|
version: process.env.RCS_VERSION || '0.1.0',
|
|
10
52
|
port: parseInt(process.env.RCS_PORT || '3000', 10),
|
|
11
53
|
host: process.env.RCS_HOST || '0.0.0.0',
|
|
@@ -22,34 +64,28 @@ export const config = {
|
|
|
22
64
|
.map(origin => origin.trim())
|
|
23
65
|
.filter(Boolean),
|
|
24
66
|
/** Whether the web UI allows open user self-registration (POST /web/auth/register).
|
|
25
|
-
* When false, only admin invitation (/join) can create new users. Default true.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
67
|
+
* When false, only admin invitation (/join) can create new users. Default true.
|
|
68
|
+
* Forced false in single-user mode — exactly one account exists. */
|
|
69
|
+
allowRegistration:
|
|
70
|
+
process.env.RCS_SINGLE_USER === 'true'
|
|
71
|
+
? false
|
|
72
|
+
: process.env.RCS_ALLOW_REGISTRATION !== 'false',
|
|
73
|
+
/** Single-user mode (`slz rcs one`): exactly one admin account
|
|
74
|
+
* (auto-created admin/admin123 on first deployment), every worker
|
|
75
|
+
* auto-owned by it, registration/join disabled, host defaults to
|
|
76
|
+
* 127.0.0.1. Local deployments only — convenience over auth. */
|
|
77
|
+
singleUser: process.env.RCS_SINGLE_USER === 'true',
|
|
30
78
|
wsIdleTimeout: parseInt(process.env.RCS_WS_IDLE_TIMEOUT || '30', 10),
|
|
31
|
-
/** Server→client keep_alive data-frame interval (seconds). Keeps reverse
|
|
32
|
-
* proxies from closing idle connections. Default 20s. */
|
|
33
79
|
wsKeepaliveInterval: parseInt(
|
|
34
80
|
process.env.RCS_WS_KEEPALIVE_INTERVAL || '20',
|
|
35
81
|
10,
|
|
36
82
|
),
|
|
37
|
-
/** Server-side client-inactivity timeout (seconds). If the bridge child
|
|
38
|
-
* process sends no data frame within this window, the server closes the
|
|
39
|
-
* WebSocket as dead. Must exceed the child's keep_alive interval
|
|
40
|
-
* (session_keepalive_interval_v2_ms, default 120s) plus jitter, otherwise
|
|
41
|
-
* healthy idle sessions get torn down between keepalives. Default 300s. */
|
|
42
83
|
wsClientInactivityTimeout: parseInt(
|
|
43
84
|
process.env.RCS_WS_CLIENT_INACTIVITY_TIMEOUT || '300',
|
|
44
85
|
10,
|
|
45
86
|
),
|
|
46
|
-
/** Whether artifact content (GET /v1/artifacts/:id/content) is served
|
|
47
|
-
* without authentication. Default true — the unguessable artifact id is
|
|
48
|
-
* the secret, same trust model as the upstream cloud-artifacts service.
|
|
49
|
-
* Set RCS_ARTIFACTS_PUBLIC_ACCESS=false to gate reads behind web session
|
|
50
|
-
* or worker credentials. */
|
|
51
87
|
artifactsPublicAccess: process.env.RCS_ARTIFACTS_PUBLIC_ACCESS !== 'false',
|
|
52
|
-
}
|
|
88
|
+
}
|
|
53
89
|
|
|
54
90
|
export function getBaseUrl(): string {
|
|
55
91
|
const url = config.baseUrl || `http://localhost:${config.port}`
|
|
@@ -13,6 +13,11 @@ import { websocket } from './transport/ws-shared'
|
|
|
13
13
|
import { getDb } from './db/sqlite'
|
|
14
14
|
import { reapExpiredSessions } from './auth/session'
|
|
15
15
|
import { cleanupExpiredArtifacts } from './services/artifacts'
|
|
16
|
+
import {
|
|
17
|
+
ensureSingleUserAdmin,
|
|
18
|
+
SINGLE_USER_DEFAULT_PASSWORD,
|
|
19
|
+
SINGLE_USER_USERNAME,
|
|
20
|
+
} from './services/single-user'
|
|
16
21
|
|
|
17
22
|
console.log('[RCS] In-memory store ready (no SQLite)')
|
|
18
23
|
|
|
@@ -39,6 +44,12 @@ app.get('/health', c => c.json({ status: 'ok', version: config.version }))
|
|
|
39
44
|
// Register all routes with Bun's serveStatic and upgradeWebSocket
|
|
40
45
|
registerRoutes(app, serveStatic, upgradeWebSocket)
|
|
41
46
|
|
|
47
|
+
// Single-user mode: local convenience — bind loopback unless the operator
|
|
48
|
+
// explicitly chose a host via RCS_HOST.
|
|
49
|
+
if (config.singleUser && !process.env.RCS_HOST) {
|
|
50
|
+
config.host = '127.0.0.1'
|
|
51
|
+
}
|
|
52
|
+
|
|
42
53
|
const port = config.port
|
|
43
54
|
const host = config.host
|
|
44
55
|
|
|
@@ -56,6 +67,36 @@ console.log(
|
|
|
56
67
|
// Start disconnect monitor
|
|
57
68
|
startDisconnectMonitor()
|
|
58
69
|
|
|
70
|
+
// Single-user mode (`slz rcs one` via bun run rcs one): bootstrap the sole
|
|
71
|
+
// admin account (admin/admin123 on first deployment) before serving
|
|
72
|
+
// requests, so worker registrations always resolve an owner.
|
|
73
|
+
if (config.singleUser) {
|
|
74
|
+
const admin = await ensureSingleUserAdmin()
|
|
75
|
+
if (!admin) {
|
|
76
|
+
console.error(
|
|
77
|
+
'[RCS] Single-user mode requires an empty or admin-bearing users ' +
|
|
78
|
+
'table, but the existing DB has neither. Run full mode (no `one` ' +
|
|
79
|
+
'argument) or point RCS_DB_PATH at a fresh database.',
|
|
80
|
+
)
|
|
81
|
+
process.exit(1)
|
|
82
|
+
}
|
|
83
|
+
console.log('[RCS] Single-user mode (one):')
|
|
84
|
+
if (admin.created) {
|
|
85
|
+
console.log(
|
|
86
|
+
`[RCS] admin account initialized — username: ${SINGLE_USER_USERNAME} password: ${SINGLE_USER_DEFAULT_PASSWORD}`,
|
|
87
|
+
)
|
|
88
|
+
console.log(
|
|
89
|
+
'[RCS] (local convenience default — change it after first login)',
|
|
90
|
+
)
|
|
91
|
+
} else {
|
|
92
|
+
console.log(`[RCS] using existing account: ${admin.username}`)
|
|
93
|
+
}
|
|
94
|
+
console.log('[RCS] registration disabled — this is the only account')
|
|
95
|
+
console.log(
|
|
96
|
+
'[RCS] workers bind zero-config and are owned by this account (no claim)',
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
59
100
|
// Reap expired session tokens at startup and periodically
|
|
60
101
|
try {
|
|
61
102
|
const db = getDb()
|