@saluzi/saluzi-edu 0.2.12 → 0.2.13
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 +212 -140
- package/dist/rcs-web/assets/Dashboard-YYwDrKTN.js +1 -0
- package/dist/rcs-web/assets/Join-DKCP5X6X.js +1 -0
- package/dist/rcs-web/assets/Login-0VGZs85I.js +1 -0
- package/{packages/remote-control-server/web/dist/assets/SessionDetail-DSoMXZJb.js → dist/rcs-web/assets/SessionDetail-DmK-DfBQ.js} +11 -11
- package/dist/rcs-web/assets/Setup-C0cjxXMt.js +1 -0
- package/dist/rcs-web/assets/{ShareLanding-C06U9Nrr.js → ShareLanding-DUZrreJ7.js} +1 -1
- package/dist/rcs-web/assets/TeamCreate-B8ZV6EdY.js +1 -0
- package/dist/rcs-web/assets/{TeamDetail-d_GwIRHd.js → TeamDetail-DBAsJ__i.js} +2 -2
- package/dist/rcs-web/assets/{TeamList-BlNGdKci.js → TeamList-C-Hte4nV.js} +1 -1
- package/dist/rcs-web/assets/{TeamSettings-CPraC9uS.js → TeamSettings-CNVwYwcp.js} +1 -1
- package/dist/rcs-web/assets/UserSettings-DRdz9kYE.js +1 -0
- package/dist/rcs-web/assets/{arc-BmYza-0n.js → arc-CJxirXEQ.js} +1 -1
- package/dist/rcs-web/assets/{architectureDiagram-3BPJPVTR-CYBtlgdO.js → architectureDiagram-3BPJPVTR-BZvIHrLA.js} +1 -1
- package/dist/rcs-web/assets/{blockDiagram-GPEHLZMM-RJAfHk_a.js → blockDiagram-GPEHLZMM-3p0y3Ija.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/c4Diagram-AAUBKEIU-CJ-qJA6w.js → dist/rcs-web/assets/c4Diagram-AAUBKEIU-Bmz9A1wn.js} +1 -1
- package/dist/rcs-web/assets/channel-DaI_n8dp.js +1 -0
- package/dist/rcs-web/assets/{chunk-2J33WTMH-n5I6Q1zJ.js → chunk-2J33WTMH-B8gQz22j.js} +1 -1
- package/dist/rcs-web/assets/{chunk-4BX2VUAB-CSOBh7qY.js → chunk-4BX2VUAB-CgLEZsTU.js} +1 -1
- package/dist/rcs-web/assets/{chunk-55IACEB6-C0euQVm_.js → chunk-55IACEB6-0mdE_-IS.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/chunk-727SXJPM-Ch7yD8cR.js → dist/rcs-web/assets/chunk-727SXJPM-DdtjnIHR.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/chunk-AQP2D5EJ-Ms2Q7r6u.js → dist/rcs-web/assets/chunk-AQP2D5EJ-BtwFPk50.js} +1 -1
- package/dist/rcs-web/assets/{chunk-FMBD7UC4-bszHUkpx.js → chunk-FMBD7UC4-BcO9mQ0P.js} +1 -1
- package/dist/rcs-web/assets/{chunk-ND2GUHAM-BVQIhq6J.js → chunk-ND2GUHAM-BvRq13S_.js} +1 -1
- package/dist/rcs-web/assets/{chunk-QZHKN3VN-BVEDe5qc.js → chunk-QZHKN3VN-D7GHkPpq.js} +1 -1
- package/dist/rcs-web/assets/classDiagram-4FO5ZUOK-pYllpkOq.js +1 -0
- package/dist/rcs-web/assets/classDiagram-v2-Q7XG4LA2-pYllpkOq.js +1 -0
- package/dist/rcs-web/assets/{code-block-IT6T5CEO-B26n-rFC.js → code-block-IT6T5CEO-B1p-vO-3.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/cose-bilkent-S5V4N54A-DXc7GE8W.js → dist/rcs-web/assets/cose-bilkent-S5V4N54A-L0rUZuH5.js} +1 -1
- package/dist/rcs-web/assets/{dagre-BM42HDAG-DAXofHt_.js → dagre-BM42HDAG-CHGuio22.js} +1 -1
- package/dist/rcs-web/assets/{diagram-2AECGRRQ-DK0TBUQA.js → diagram-2AECGRRQ-DIDRyQk8.js} +1 -1
- package/dist/rcs-web/assets/{diagram-5GNKFQAL-DMsq-Grp.js → diagram-5GNKFQAL-CNubKb6P.js} +1 -1
- package/dist/rcs-web/assets/{diagram-KO2AKTUF-B3FMhmUc.js → diagram-KO2AKTUF-CzQh664-.js} +1 -1
- package/dist/rcs-web/assets/{diagram-LMA3HP47-BjO3BHFl.js → diagram-LMA3HP47-CWBR7-VG.js} +1 -1
- package/dist/rcs-web/assets/{diagram-OG6HWLK6-CNg4B2L3.js → diagram-OG6HWLK6-BMHdlbcZ.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/erDiagram-TEJ5UH35-BDxz8NRK.js → dist/rcs-web/assets/erDiagram-TEJ5UH35-OgT8dC-K.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/flowDiagram-I6XJVG4X-VIgM13sK.js → dist/rcs-web/assets/flowDiagram-I6XJVG4X-DRrCY2oB.js} +1 -1
- package/dist/rcs-web/assets/{ganttDiagram-6RSMTGT7-Ba2QuJU0.js → ganttDiagram-6RSMTGT7-DctpzEKN.js} +1 -1
- package/dist/rcs-web/assets/{gitGraphDiagram-PVQCEYII-B-gV-lAg.js → gitGraphDiagram-PVQCEYII-5KpLszm9.js} +1 -1
- package/dist/rcs-web/assets/{infoDiagram-5YYISTIA-DothwgD4.js → infoDiagram-5YYISTIA-BAKD_Tt7.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/ishikawaDiagram-YF4QCWOH-DhpjM1Vc.js → dist/rcs-web/assets/ishikawaDiagram-YF4QCWOH-VHOPG4Nc.js} +1 -1
- package/dist/rcs-web/assets/{journeyDiagram-JHISSGLW-PbQTouMJ.js → journeyDiagram-JHISSGLW-BtGih-ik.js} +1 -1
- package/dist/rcs-web/assets/{kanban-definition-UN3LZRKU-63WdwG-X.js → kanban-definition-UN3LZRKU-BsEGZsD1.js} +1 -1
- package/dist/rcs-web/assets/{linear-CeRljV9s.js → linear-Dj636PNG.js} +1 -1
- package/dist/rcs-web/assets/main-CkqtCllF.js +238 -0
- package/dist/rcs-web/assets/{mermaid-VLURNSYL-CG6yNR7g.js → mermaid-VLURNSYL-DutpHHm9.js} +4 -4
- package/dist/rcs-web/assets/{mermaid.core-CAuNNATY.js → mermaid.core-XHHf0pen.js} +5 -5
- package/dist/rcs-web/assets/{mindmap-definition-RKZ34NQL-9hDL1CUV.js → mindmap-definition-RKZ34NQL-BBa8V_3g.js} +1 -1
- package/dist/rcs-web/assets/{pieDiagram-4H26LBE5-BS_P2t04.js → pieDiagram-4H26LBE5-BuNEdo9H.js} +1 -1
- package/dist/rcs-web/assets/{quadrantDiagram-W4KKPZXB-Cp9QQkHV.js → quadrantDiagram-W4KKPZXB-CsJgyQIN.js} +1 -1
- package/dist/rcs-web/assets/{requirementDiagram-4Y6WPE33-C_XSzOKE.js → requirementDiagram-4Y6WPE33-DqMJZ914.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/sankeyDiagram-5OEKKPKP-BhuVqzZd.js → dist/rcs-web/assets/sankeyDiagram-5OEKKPKP-CVsz09q2.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/sequenceDiagram-3UESZ5HK-C3IPZrcW.js → dist/rcs-web/assets/sequenceDiagram-3UESZ5HK-BRp4Wddn.js} +1 -1
- package/dist/rcs-web/assets/{stateDiagram-AJRCARHV-CN4iNPRl.js → stateDiagram-AJRCARHV-S-QqQr-D.js} +1 -1
- package/dist/rcs-web/assets/{stateDiagram-v2-BHNVJYJU-CICBlDSJ.js → stateDiagram-v2-BHNVJYJU-D77KfDr-.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/timeline-definition-PNZ67QCA-Be7t_3rC.js → dist/rcs-web/assets/timeline-definition-PNZ67QCA-De8bFWnE.js} +1 -1
- package/dist/rcs-web/assets/{useTeams-DPHGnBHL.js → useTeams-CqJOqEy1.js} +1 -1
- package/dist/rcs-web/assets/{vennDiagram-CIIHVFJN-CSaUc2Xv.js → vennDiagram-CIIHVFJN-Dikwrn1L.js} +1 -1
- package/dist/rcs-web/assets/{wardleyDiagram-YWT4CUSO-DTuJrl9P.js → wardleyDiagram-YWT4CUSO-DUzpiV6H.js} +1 -1
- package/dist/rcs-web/assets/{workerType-CSqeySYe.js → workerType-DOQ0AHrq.js} +1 -1
- package/dist/rcs-web/assets/{xychartDiagram-2RQKCTM6-BQ0oU3Qi.js → xychartDiagram-2RQKCTM6-OZNbB32D.js} +1 -1
- package/dist/rcs-web/index.html +1 -1
- package/package.json +2 -2
- package/packages/remote-control-server/src/__tests__/cookie.test.ts +0 -71
- package/packages/remote-control-server/src/__tests__/data-layer-round2.test.ts +556 -0
- package/packages/remote-control-server/src/__tests__/route-mounting.test.ts +212 -0
- package/packages/remote-control-server/src/__tests__/routes.test.ts +5 -44
- package/packages/remote-control-server/src/__tests__/security-fixes.test.ts +526 -0
- package/packages/remote-control-server/src/__tests__/security-round3.test.ts +780 -0
- package/packages/remote-control-server/src/__tests__/shares.test.ts +5 -2
- package/packages/remote-control-server/src/auth/constants.ts +0 -6
- package/packages/remote-control-server/src/auth/cookie.ts +0 -43
- package/packages/remote-control-server/src/auth/csrf.ts +58 -0
- package/packages/remote-control-server/src/auth/middleware.ts +7 -4
- package/packages/remote-control-server/src/index.ts +26 -0
- package/packages/remote-control-server/src/registerRoutes.ts +17 -1
- package/packages/remote-control-server/src/routes/web/auth-routes.ts +93 -54
- package/packages/remote-control-server/src/routes/web/auth.ts +16 -23
- package/packages/remote-control-server/src/routes/web/sessions.ts +30 -5
- package/packages/remote-control-server/src/routes/web/shares.ts +38 -1
- package/packages/remote-control-server/src/routes/web/teams.ts +22 -4
- package/packages/remote-control-server/src/services/session.ts +143 -9
- package/packages/remote-control-server/src/store.ts +34 -1
- package/packages/remote-control-server/src/types/api.ts +0 -1
- package/packages/remote-control-server/web/dist/assets/Dashboard-YYwDrKTN.js +1 -0
- package/packages/remote-control-server/web/dist/assets/Join-DKCP5X6X.js +1 -0
- package/packages/remote-control-server/web/dist/assets/Login-0VGZs85I.js +1 -0
- package/{dist/rcs-web/assets/SessionDetail-DSoMXZJb.js → packages/remote-control-server/web/dist/assets/SessionDetail-DmK-DfBQ.js} +11 -11
- package/packages/remote-control-server/web/dist/assets/Setup-C0cjxXMt.js +1 -0
- package/packages/remote-control-server/web/dist/assets/{ShareLanding-C06U9Nrr.js → ShareLanding-DUZrreJ7.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/TeamCreate-B8ZV6EdY.js +1 -0
- package/packages/remote-control-server/web/dist/assets/{TeamDetail-d_GwIRHd.js → TeamDetail-DBAsJ__i.js} +2 -2
- package/packages/remote-control-server/web/dist/assets/{TeamList-BlNGdKci.js → TeamList-C-Hte4nV.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{TeamSettings-CPraC9uS.js → TeamSettings-CNVwYwcp.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/UserSettings-DRdz9kYE.js +1 -0
- package/packages/remote-control-server/web/dist/assets/{arc-BmYza-0n.js → arc-CJxirXEQ.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{architectureDiagram-3BPJPVTR-CYBtlgdO.js → architectureDiagram-3BPJPVTR-BZvIHrLA.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{blockDiagram-GPEHLZMM-RJAfHk_a.js → blockDiagram-GPEHLZMM-3p0y3Ija.js} +1 -1
- package/{dist/rcs-web/assets/c4Diagram-AAUBKEIU-CJ-qJA6w.js → packages/remote-control-server/web/dist/assets/c4Diagram-AAUBKEIU-Bmz9A1wn.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/channel-DaI_n8dp.js +1 -0
- package/packages/remote-control-server/web/dist/assets/{chunk-2J33WTMH-n5I6Q1zJ.js → chunk-2J33WTMH-B8gQz22j.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-4BX2VUAB-CSOBh7qY.js → chunk-4BX2VUAB-CgLEZsTU.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-55IACEB6-C0euQVm_.js → chunk-55IACEB6-0mdE_-IS.js} +1 -1
- package/{dist/rcs-web/assets/chunk-727SXJPM-Ch7yD8cR.js → packages/remote-control-server/web/dist/assets/chunk-727SXJPM-DdtjnIHR.js} +1 -1
- package/{dist/rcs-web/assets/chunk-AQP2D5EJ-Ms2Q7r6u.js → packages/remote-control-server/web/dist/assets/chunk-AQP2D5EJ-BtwFPk50.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-FMBD7UC4-bszHUkpx.js → chunk-FMBD7UC4-BcO9mQ0P.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-ND2GUHAM-BVQIhq6J.js → chunk-ND2GUHAM-BvRq13S_.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-QZHKN3VN-BVEDe5qc.js → chunk-QZHKN3VN-D7GHkPpq.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/classDiagram-4FO5ZUOK-pYllpkOq.js +1 -0
- package/packages/remote-control-server/web/dist/assets/classDiagram-v2-Q7XG4LA2-pYllpkOq.js +1 -0
- package/packages/remote-control-server/web/dist/assets/{code-block-IT6T5CEO-B26n-rFC.js → code-block-IT6T5CEO-B1p-vO-3.js} +1 -1
- package/{dist/rcs-web/assets/cose-bilkent-S5V4N54A-DXc7GE8W.js → packages/remote-control-server/web/dist/assets/cose-bilkent-S5V4N54A-L0rUZuH5.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{dagre-BM42HDAG-DAXofHt_.js → dagre-BM42HDAG-CHGuio22.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{diagram-2AECGRRQ-DK0TBUQA.js → diagram-2AECGRRQ-DIDRyQk8.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{diagram-5GNKFQAL-DMsq-Grp.js → diagram-5GNKFQAL-CNubKb6P.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{diagram-KO2AKTUF-B3FMhmUc.js → diagram-KO2AKTUF-CzQh664-.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{diagram-LMA3HP47-BjO3BHFl.js → diagram-LMA3HP47-CWBR7-VG.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{diagram-OG6HWLK6-CNg4B2L3.js → diagram-OG6HWLK6-BMHdlbcZ.js} +1 -1
- package/{dist/rcs-web/assets/erDiagram-TEJ5UH35-BDxz8NRK.js → packages/remote-control-server/web/dist/assets/erDiagram-TEJ5UH35-OgT8dC-K.js} +1 -1
- package/{dist/rcs-web/assets/flowDiagram-I6XJVG4X-VIgM13sK.js → packages/remote-control-server/web/dist/assets/flowDiagram-I6XJVG4X-DRrCY2oB.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{ganttDiagram-6RSMTGT7-Ba2QuJU0.js → ganttDiagram-6RSMTGT7-DctpzEKN.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{gitGraphDiagram-PVQCEYII-B-gV-lAg.js → gitGraphDiagram-PVQCEYII-5KpLszm9.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{infoDiagram-5YYISTIA-DothwgD4.js → infoDiagram-5YYISTIA-BAKD_Tt7.js} +1 -1
- package/{dist/rcs-web/assets/ishikawaDiagram-YF4QCWOH-DhpjM1Vc.js → packages/remote-control-server/web/dist/assets/ishikawaDiagram-YF4QCWOH-VHOPG4Nc.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{journeyDiagram-JHISSGLW-PbQTouMJ.js → journeyDiagram-JHISSGLW-BtGih-ik.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{kanban-definition-UN3LZRKU-63WdwG-X.js → kanban-definition-UN3LZRKU-BsEGZsD1.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{linear-CeRljV9s.js → linear-Dj636PNG.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/main-CkqtCllF.js +238 -0
- package/packages/remote-control-server/web/dist/assets/{mermaid-VLURNSYL-CG6yNR7g.js → mermaid-VLURNSYL-DutpHHm9.js} +4 -4
- package/packages/remote-control-server/web/dist/assets/{mermaid.core-CAuNNATY.js → mermaid.core-XHHf0pen.js} +5 -5
- package/packages/remote-control-server/web/dist/assets/{mindmap-definition-RKZ34NQL-9hDL1CUV.js → mindmap-definition-RKZ34NQL-BBa8V_3g.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{pieDiagram-4H26LBE5-BS_P2t04.js → pieDiagram-4H26LBE5-BuNEdo9H.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{quadrantDiagram-W4KKPZXB-Cp9QQkHV.js → quadrantDiagram-W4KKPZXB-CsJgyQIN.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{requirementDiagram-4Y6WPE33-C_XSzOKE.js → requirementDiagram-4Y6WPE33-DqMJZ914.js} +1 -1
- package/{dist/rcs-web/assets/sankeyDiagram-5OEKKPKP-BhuVqzZd.js → packages/remote-control-server/web/dist/assets/sankeyDiagram-5OEKKPKP-CVsz09q2.js} +1 -1
- package/{dist/rcs-web/assets/sequenceDiagram-3UESZ5HK-C3IPZrcW.js → packages/remote-control-server/web/dist/assets/sequenceDiagram-3UESZ5HK-BRp4Wddn.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{stateDiagram-AJRCARHV-CN4iNPRl.js → stateDiagram-AJRCARHV-S-QqQr-D.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{stateDiagram-v2-BHNVJYJU-CICBlDSJ.js → stateDiagram-v2-BHNVJYJU-D77KfDr-.js} +1 -1
- package/{dist/rcs-web/assets/timeline-definition-PNZ67QCA-Be7t_3rC.js → packages/remote-control-server/web/dist/assets/timeline-definition-PNZ67QCA-De8bFWnE.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{useTeams-DPHGnBHL.js → useTeams-CqJOqEy1.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{vennDiagram-CIIHVFJN-CSaUc2Xv.js → vennDiagram-CIIHVFJN-Dikwrn1L.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{wardleyDiagram-YWT4CUSO-DTuJrl9P.js → wardleyDiagram-YWT4CUSO-DUzpiV6H.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{workerType-CSqeySYe.js → workerType-DOQ0AHrq.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{xychartDiagram-2RQKCTM6-BQ0oU3Qi.js → xychartDiagram-2RQKCTM6-OZNbB32D.js} +1 -1
- package/packages/remote-control-server/web/dist/index.html +1 -1
- package/dist/rcs-web/assets/Dashboard-D_Im4UId.js +0 -1
- package/dist/rcs-web/assets/Join-BTnvMsB9.js +0 -1
- package/dist/rcs-web/assets/Login-ClT7RckC.js +0 -1
- package/dist/rcs-web/assets/Setup-PaDA6Et-.js +0 -1
- package/dist/rcs-web/assets/TeamCreate-249rK3U3.js +0 -1
- package/dist/rcs-web/assets/UserSettings-X627Fdni.js +0 -1
- package/dist/rcs-web/assets/channel-BUwWbT6U.js +0 -1
- package/dist/rcs-web/assets/classDiagram-4FO5ZUOK-DYtx22zv.js +0 -1
- package/dist/rcs-web/assets/classDiagram-v2-Q7XG4LA2-DYtx22zv.js +0 -1
- package/dist/rcs-web/assets/i18n-Dbt-vEQ3.js +0 -1
- package/dist/rcs-web/assets/main-DvfxciBy.js +0 -238
- package/packages/remote-control-server/web/dist/assets/Dashboard-D_Im4UId.js +0 -1
- package/packages/remote-control-server/web/dist/assets/Join-BTnvMsB9.js +0 -1
- package/packages/remote-control-server/web/dist/assets/Login-ClT7RckC.js +0 -1
- package/packages/remote-control-server/web/dist/assets/Setup-PaDA6Et-.js +0 -1
- package/packages/remote-control-server/web/dist/assets/TeamCreate-249rK3U3.js +0 -1
- package/packages/remote-control-server/web/dist/assets/UserSettings-X627Fdni.js +0 -1
- package/packages/remote-control-server/web/dist/assets/channel-BUwWbT6U.js +0 -1
- package/packages/remote-control-server/web/dist/assets/classDiagram-4FO5ZUOK-DYtx22zv.js +0 -1
- package/packages/remote-control-server/web/dist/assets/classDiagram-v2-Q7XG4LA2-DYtx22zv.js +0 -1
- package/packages/remote-control-server/web/dist/assets/i18n-Dbt-vEQ3.js +0 -1
- package/packages/remote-control-server/web/dist/assets/main-DvfxciBy.js +0 -238
|
@@ -1021,8 +1021,11 @@ describe('share token access control', () => {
|
|
|
1021
1021
|
method: 'GET',
|
|
1022
1022
|
headers: authHeader(userA.accessToken),
|
|
1023
1023
|
})
|
|
1024
|
-
//
|
|
1025
|
-
|
|
1024
|
+
// Share-based access is now implemented — the session resolves via the
|
|
1025
|
+
// share grant (not 403). This fixture only seeds SQLite (not the in-memory
|
|
1026
|
+
// Map), so getSession() returns null → 404. A full integration test with
|
|
1027
|
+
// the session in memory would expect 200.
|
|
1028
|
+
expect([200, 404]).toContain(res.status)
|
|
1026
1029
|
})
|
|
1027
1030
|
|
|
1028
1031
|
test('write permission → can send messages', async () => {
|
|
@@ -3,11 +3,5 @@
|
|
|
3
3
|
*
|
|
4
4
|
* SYSTEM_USER_ID is the identity assigned to API-key-authenticated operations.
|
|
5
5
|
* These operations are not attributable to any specific human user.
|
|
6
|
-
*
|
|
7
|
-
* ADMIN_FALLBACK_USER_ID is kept only for backward compatibility in code paths
|
|
8
|
-
* that previously relied on the literal string. New code should use SYSTEM_USER_ID.
|
|
9
6
|
*/
|
|
10
7
|
export const SYSTEM_USER_ID = '__system__'
|
|
11
|
-
|
|
12
|
-
/** @deprecated Use SYSTEM_USER_ID for new code. Kept for backward compat only. */
|
|
13
|
-
export const ADMIN_FALLBACK_USER_ID = '__admin__'
|
|
@@ -1,46 +1,3 @@
|
|
|
1
|
-
import { createHmac, timingSafeEqual } from 'node:crypto'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Sign cookie data with HMAC-SHA256.
|
|
5
|
-
* Returns `data.signature` format.
|
|
6
|
-
*
|
|
7
|
-
* Note: Currently unused by production code — token verification relies on
|
|
8
|
-
* database lookup. Kept as utility for potential future use.
|
|
9
|
-
*/
|
|
10
|
-
export function signCookie(data: string, secret: string): string {
|
|
11
|
-
const sig = createHmac('sha256', secret).update(data).digest('hex')
|
|
12
|
-
return `${data}.${sig}`
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Parse and verify a signed cookie value.
|
|
17
|
-
* Returns the original data if signature is valid, null otherwise.
|
|
18
|
-
*
|
|
19
|
-
* Note: Currently unused by production code — token verification relies on
|
|
20
|
-
* database lookup. Kept as utility for potential future use.
|
|
21
|
-
*/
|
|
22
|
-
export function parseCookie(
|
|
23
|
-
signedValue: string,
|
|
24
|
-
secret: string,
|
|
25
|
-
): string | null {
|
|
26
|
-
if (!signedValue) return null
|
|
27
|
-
|
|
28
|
-
const lastDot = signedValue.lastIndexOf('.')
|
|
29
|
-
if (lastDot === -1) return null
|
|
30
|
-
|
|
31
|
-
const data = signedValue.slice(0, lastDot)
|
|
32
|
-
const sig = signedValue.slice(lastDot + 1)
|
|
33
|
-
|
|
34
|
-
const expectedSig = createHmac('sha256', secret).update(data).digest('hex')
|
|
35
|
-
|
|
36
|
-
const a = Buffer.from(sig, 'utf8')
|
|
37
|
-
const b = Buffer.from(expectedSig, 'utf8')
|
|
38
|
-
if (a.length !== b.length) return null
|
|
39
|
-
if (!timingSafeEqual(a, b)) return null
|
|
40
|
-
|
|
41
|
-
return data
|
|
42
|
-
}
|
|
43
|
-
|
|
44
1
|
/**
|
|
45
2
|
* Build Set-Cookie header values for access and refresh tokens.
|
|
46
3
|
*
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Context, Next } from 'hono'
|
|
2
|
+
|
|
3
|
+
const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS'])
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CSRF protection middleware.
|
|
7
|
+
*
|
|
8
|
+
* For state-changing methods (POST/PUT/DELETE/PATCH), validates the Origin
|
|
9
|
+
* header against the request host and any explicitly allowed origins.
|
|
10
|
+
*
|
|
11
|
+
* Non-browser clients (curl, CLI, SDK) that don't send Origin are allowed
|
|
12
|
+
* through — they rely on Bearer/API-key authentication instead.
|
|
13
|
+
*/
|
|
14
|
+
export async function csrfCheck(c: Context, next: Next) {
|
|
15
|
+
if (SAFE_METHODS.has(c.req.method)) {
|
|
16
|
+
await next()
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const origin = c.req.header('Origin')
|
|
21
|
+
if (!origin) {
|
|
22
|
+
// Non-browser clients (no Origin header) — pass through;
|
|
23
|
+
// authentication is enforced by downstream middleware.
|
|
24
|
+
await next()
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let host: string
|
|
29
|
+
try {
|
|
30
|
+
host = new URL(origin).origin
|
|
31
|
+
} catch {
|
|
32
|
+
return c.json({ error: 'CSRF check failed' }, 403)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Build the allowed origins list: same-origin + ALLOWED_ORIGINS env
|
|
36
|
+
const requestUrl = new URL(c.req.url)
|
|
37
|
+
const selfOrigin = requestUrl.origin
|
|
38
|
+
const allowed: string[] = [selfOrigin]
|
|
39
|
+
|
|
40
|
+
// Support ALLOWED_ORIGINS env variable (comma-separated)
|
|
41
|
+
const envOrigins =
|
|
42
|
+
(c.env?.ALLOWED_ORIGINS as string | undefined) ??
|
|
43
|
+
(typeof process !== 'undefined'
|
|
44
|
+
? process.env.ALLOWED_ORIGINS
|
|
45
|
+
: undefined)
|
|
46
|
+
if (envOrigins) {
|
|
47
|
+
for (const o of envOrigins.split(',')) {
|
|
48
|
+
const trimmed = o.trim()
|
|
49
|
+
if (trimmed) allowed.push(trimmed)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!allowed.includes(host)) {
|
|
54
|
+
return c.json({ error: 'CSRF check failed' }, 403)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await next()
|
|
58
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Context, Next } from 'hono'
|
|
2
|
+
import { getCookie } from 'hono/cookie'
|
|
2
3
|
import { validateApiKey } from './api-key'
|
|
3
4
|
import { verifyWorkerJwt } from './jwt'
|
|
4
5
|
import { resolveToken } from './token'
|
|
@@ -192,9 +193,7 @@ export async function sessionAuth(c: Context, next: Next) {
|
|
|
192
193
|
}
|
|
193
194
|
|
|
194
195
|
// Try cookie-based token
|
|
195
|
-
const
|
|
196
|
-
const accessMatch = cookieHeader.match(/rcs_access=([^;]+)/)
|
|
197
|
-
const cookieToken = accessMatch ? accessMatch[1] : undefined
|
|
196
|
+
const cookieToken = getCookie(c, 'rcs_access')
|
|
198
197
|
|
|
199
198
|
const tokenToResolve = cookieToken || bearer
|
|
200
199
|
|
|
@@ -213,8 +212,12 @@ export async function sessionAuth(c: Context, next: Next) {
|
|
|
213
212
|
const db = getDb()
|
|
214
213
|
const userId = resolveSessionToken(tokenToResolve, db)
|
|
215
214
|
if (userId) {
|
|
215
|
+
// Look up role from DB to set isAdmin
|
|
216
|
+
const userRow = db
|
|
217
|
+
.query('SELECT role FROM users WHERE id = $userId')
|
|
218
|
+
.get({ $userId: userId }) as { role: string } | null
|
|
216
219
|
c.set('userId', userId)
|
|
217
|
-
c.set('isAdmin',
|
|
220
|
+
c.set('isAdmin', userRow?.role === 'admin')
|
|
218
221
|
await next()
|
|
219
222
|
return
|
|
220
223
|
}
|
|
@@ -10,6 +10,8 @@ import { startDisconnectMonitor } from './services/disconnect-monitor'
|
|
|
10
10
|
import { registerRoutes } from './registerRoutes'
|
|
11
11
|
import { webCorsOptions } from './auth/cors'
|
|
12
12
|
import { websocket } from './transport/ws-shared'
|
|
13
|
+
import { getDb } from './db/sqlite'
|
|
14
|
+
import { reapExpiredSessions } from './auth/session'
|
|
13
15
|
|
|
14
16
|
console.log('[RCS] In-memory store ready (no SQLite)')
|
|
15
17
|
|
|
@@ -53,6 +55,30 @@ console.log(
|
|
|
53
55
|
// Start disconnect monitor
|
|
54
56
|
startDisconnectMonitor()
|
|
55
57
|
|
|
58
|
+
// Reap expired session tokens at startup and periodically
|
|
59
|
+
try {
|
|
60
|
+
const db = getDb()
|
|
61
|
+
const reaped = reapExpiredSessions(db)
|
|
62
|
+
if (reaped > 0) {
|
|
63
|
+
console.log(`[RCS] Reaped ${reaped} expired session tokens`)
|
|
64
|
+
}
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.warn('[RCS] Failed to reap expired sessions:', err)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const reapInterval = setInterval(() => {
|
|
70
|
+
try {
|
|
71
|
+
const db = getDb()
|
|
72
|
+
reapExpiredSessions(db)
|
|
73
|
+
} catch {
|
|
74
|
+
// ignore — DB may not be available
|
|
75
|
+
}
|
|
76
|
+
}, 60 * 60 * 1000) // every hour
|
|
77
|
+
|
|
78
|
+
if (reapInterval.unref) {
|
|
79
|
+
reapInterval.unref()
|
|
80
|
+
}
|
|
81
|
+
|
|
56
82
|
export default {
|
|
57
83
|
port,
|
|
58
84
|
hostname: host,
|
|
@@ -6,6 +6,7 @@ import { existsSync, appendFileSync } from 'node:fs'
|
|
|
6
6
|
import { fileURLToPath } from 'node:url'
|
|
7
7
|
import { createAcpApp } from './routes/acp'
|
|
8
8
|
import { createSessionIngressApp } from './routes/v1/session-ingress'
|
|
9
|
+
import { csrfCheck } from './auth/csrf'
|
|
9
10
|
|
|
10
11
|
// Routes
|
|
11
12
|
import v1Environments from './routes/v1/environments'
|
|
@@ -16,9 +17,12 @@ import v2Worker from './routes/v2/worker'
|
|
|
16
17
|
import v2WorkerEventsStream from './routes/v2/worker-events-stream'
|
|
17
18
|
import v2WorkerEvents from './routes/v2/worker-events'
|
|
18
19
|
import webAuth from './routes/web/auth'
|
|
20
|
+
import webAuthRoutes from './routes/web/auth-routes'
|
|
19
21
|
import webSessions from './routes/web/sessions'
|
|
20
22
|
import webControl from './routes/web/control'
|
|
21
23
|
import webEnvironments from './routes/web/environments'
|
|
24
|
+
import webShares from './routes/web/shares'
|
|
25
|
+
import webTeams from './routes/web/teams'
|
|
22
26
|
|
|
23
27
|
type ServeStaticFn = (opts: Record<string, unknown>) => MiddlewareHandler
|
|
24
28
|
|
|
@@ -72,8 +76,12 @@ export function registerRoutes(
|
|
|
72
76
|
app.route('/v1/code/sessions', v2WorkerEventsStream)
|
|
73
77
|
app.route('/v1/code/sessions', v2WorkerEvents)
|
|
74
78
|
|
|
75
|
-
// Web control panel routes
|
|
79
|
+
// Web control panel routes — CSRF protection applied before route handlers
|
|
80
|
+
app.use('/web/*', csrfCheck)
|
|
76
81
|
app.route('/web', webAuth)
|
|
82
|
+
app.route('/web/auth', webAuthRoutes)
|
|
83
|
+
app.route('/web', webShares)
|
|
84
|
+
app.route('/web', webTeams)
|
|
77
85
|
app.route('/web', webSessions)
|
|
78
86
|
app.route('/web', webControl)
|
|
79
87
|
app.route('/web', webEnvironments)
|
|
@@ -96,4 +104,12 @@ export function registerRoutes(
|
|
|
96
104
|
return c.json({ ok: false, error: String(err) }, 500)
|
|
97
105
|
}
|
|
98
106
|
})
|
|
107
|
+
|
|
108
|
+
// SPA fallback — serve index.html for frontend routes handled by React Router.
|
|
109
|
+
// Must be registered AFTER all API routes to avoid intercepting /web/*, /v1/*, /acp/* etc.
|
|
110
|
+
const spaPaths = ['/login', '/setup', '/join', '/settings', '/teams']
|
|
111
|
+
for (const p of spaPaths) {
|
|
112
|
+
app.get(p, ss({ root: webDir, path: 'index.html' }))
|
|
113
|
+
app.get(`${p}/*`, ss({ root: webDir, path: 'index.html' }))
|
|
114
|
+
}
|
|
99
115
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Hono } from 'hono'
|
|
2
|
+
import { getCookie } from 'hono/cookie'
|
|
2
3
|
import { randomUUID, createHash } from 'node:crypto'
|
|
3
4
|
import { config } from '../../config'
|
|
4
5
|
import { getDb } from '../../db/sqlite'
|
|
5
6
|
import {
|
|
6
|
-
hashPassword,
|
|
7
7
|
hashPasswordAsync,
|
|
8
8
|
verifyPassword,
|
|
9
9
|
verifyPasswordAsync,
|
|
@@ -19,6 +19,25 @@ import { SYSTEM_USER_ID } from '../../auth/constants'
|
|
|
19
19
|
|
|
20
20
|
const app = new Hono()
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Detect whether the current request is over HTTPS, taking reverse-proxy
|
|
24
|
+
* headers (X-Forwarded-Proto) into account.
|
|
25
|
+
*/
|
|
26
|
+
function isSecureRequest(c: import('hono').Context): boolean {
|
|
27
|
+
const forwardedProto = c.req.header('X-Forwarded-Proto')
|
|
28
|
+
if (forwardedProto === 'https') return true
|
|
29
|
+
return c.req.url.startsWith('https://')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Dummy argon2id hash used for timing-safe login when the user does not
|
|
34
|
+
* exist. Prevents timing side-channel attacks that would reveal whether
|
|
35
|
+
* a username is registered. The value is a valid PHC-format argon2id hash
|
|
36
|
+
* that will never match any real password.
|
|
37
|
+
*/
|
|
38
|
+
const DUMMY_HASH =
|
|
39
|
+
'$argon2id$v=19$m=65536,t=3,p=1$dGVzdHNhbHQ$dGVzdGhhc2h0ZXN0aGFzaA'
|
|
40
|
+
|
|
22
41
|
// ---------------------------------------------------------------------------
|
|
23
42
|
// Login rate limiting (Task 2) — in-memory, per IP+username
|
|
24
43
|
// ---------------------------------------------------------------------------
|
|
@@ -180,7 +199,7 @@ app.post('/setup', async c => {
|
|
|
180
199
|
// serial write model. The first request INSERTs (changes=1), the second
|
|
181
200
|
// finds the admin row already present (changes=0 → 409).
|
|
182
201
|
const userId = `usr_${randomUUID().replace(/-/g, '')}`
|
|
183
|
-
const passwordHash =
|
|
202
|
+
const passwordHash = await hashPasswordAsync(password)
|
|
184
203
|
const now = new Date().toISOString()
|
|
185
204
|
|
|
186
205
|
try {
|
|
@@ -205,7 +224,7 @@ app.post('/setup', async c => {
|
|
|
205
224
|
return c.json({ error: 'Setup failed' }, 500)
|
|
206
225
|
}
|
|
207
226
|
|
|
208
|
-
const isHttps = c
|
|
227
|
+
const isHttps = isSecureRequest(c)
|
|
209
228
|
return issueAndRespond(c, userId, isHttps)
|
|
210
229
|
})
|
|
211
230
|
|
|
@@ -241,8 +260,16 @@ app.post('/login', async c => {
|
|
|
241
260
|
.query('SELECT * FROM users WHERE username = $username')
|
|
242
261
|
.get({ $username: username }) as UserRow | null
|
|
243
262
|
|
|
244
|
-
// Generic error to avoid leaking user existence
|
|
245
|
-
|
|
263
|
+
// Generic error to avoid leaking user existence.
|
|
264
|
+
// When user doesn't exist, still run verifyPassword against a dummy hash
|
|
265
|
+
// to prevent timing side-channel attacks.
|
|
266
|
+
if (!user) {
|
|
267
|
+
verifyPassword(password, DUMMY_HASH)
|
|
268
|
+
recordFailedAttempt(clientIp, username)
|
|
269
|
+
return c.json({ error: 'Invalid credentials' }, 401)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (!verifyPassword(password, user.password_hash)) {
|
|
246
273
|
recordFailedAttempt(clientIp, username)
|
|
247
274
|
return c.json({ error: 'Invalid credentials' }, 401)
|
|
248
275
|
}
|
|
@@ -256,7 +283,7 @@ app.post('/login', async c => {
|
|
|
256
283
|
$id: user.id,
|
|
257
284
|
})
|
|
258
285
|
|
|
259
|
-
const isHttps = c
|
|
286
|
+
const isHttps = isSecureRequest(c)
|
|
260
287
|
return issueAndRespond(c, user.id, isHttps)
|
|
261
288
|
})
|
|
262
289
|
|
|
@@ -273,7 +300,7 @@ app.post('/refresh', async c => {
|
|
|
273
300
|
const refreshToken =
|
|
274
301
|
(body.refreshToken as string) ||
|
|
275
302
|
(body.refresh_token as string) ||
|
|
276
|
-
|
|
303
|
+
getCookie(c, 'rcs_refresh')
|
|
277
304
|
|
|
278
305
|
if (!refreshToken) {
|
|
279
306
|
return c.json({ error: 'Refresh token required' }, 401)
|
|
@@ -290,7 +317,7 @@ app.post('/refresh', async c => {
|
|
|
290
317
|
revokeSessionToken(refreshToken, db)
|
|
291
318
|
|
|
292
319
|
// Issue new access + refresh token pair
|
|
293
|
-
const isHttps = c
|
|
320
|
+
const isHttps = isSecureRequest(c)
|
|
294
321
|
const { accessToken, refreshToken: newRefreshToken } = issueSessionToken(
|
|
295
322
|
userId,
|
|
296
323
|
db,
|
|
@@ -317,7 +344,7 @@ app.post('/logout', async c => {
|
|
|
317
344
|
? authHeader.slice('Bearer '.length)
|
|
318
345
|
: undefined
|
|
319
346
|
|
|
320
|
-
const cookieToken =
|
|
347
|
+
const cookieToken = getCookie(c, 'rcs_access')
|
|
321
348
|
|
|
322
349
|
const token = bearerToken || cookieToken
|
|
323
350
|
|
|
@@ -326,7 +353,18 @@ app.post('/logout', async c => {
|
|
|
326
353
|
const db = getDb()
|
|
327
354
|
revokeSessionToken(token, db)
|
|
328
355
|
} catch (err) {
|
|
329
|
-
console.warn('[/logout] failed to revoke token:', err)
|
|
356
|
+
console.warn('[/logout] failed to revoke access token:', err)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Also revoke the refresh token if present
|
|
361
|
+
const refreshToken = getCookie(c, 'rcs_refresh')
|
|
362
|
+
if (refreshToken) {
|
|
363
|
+
try {
|
|
364
|
+
const db = getDb()
|
|
365
|
+
revokeSessionToken(refreshToken, db)
|
|
366
|
+
} catch {
|
|
367
|
+
// ignore — refresh token may already be invalid
|
|
330
368
|
}
|
|
331
369
|
}
|
|
332
370
|
|
|
@@ -387,66 +425,73 @@ app.post('/join', async c => {
|
|
|
387
425
|
return c.json({ error: 'Invitation expired' }, 400)
|
|
388
426
|
}
|
|
389
427
|
|
|
390
|
-
// Check if already fully consumed (pre-check; atomic check below)
|
|
391
|
-
|
|
428
|
+
// Check if already fully consumed (pre-check; atomic check below).
|
|
429
|
+
// Only check uses >= max_uses — consumed_at is set only when the last
|
|
430
|
+
// slot is used, so checking it here would break max_uses > 1 invitations.
|
|
431
|
+
if (invitation.uses >= invitation.max_uses) {
|
|
392
432
|
return c.json({ error: 'Invitation already used' }, 400)
|
|
393
433
|
}
|
|
394
434
|
|
|
395
|
-
// Check username uniqueness
|
|
435
|
+
// Check username uniqueness (pre-check; atomic check inside transaction)
|
|
396
436
|
const existingUser = db
|
|
397
437
|
.query('SELECT id FROM users WHERE username = $username')
|
|
398
438
|
.get({ $username: username })
|
|
399
439
|
if (existingUser) {
|
|
400
|
-
return c.json({ error: 'Username already taken' },
|
|
440
|
+
return c.json({ error: 'Username already taken' }, 409)
|
|
401
441
|
}
|
|
402
442
|
|
|
403
443
|
// Create user + atomically consume invitation slot in a transaction.
|
|
404
|
-
// The UPDATE with `uses < max_uses
|
|
405
|
-
//
|
|
406
|
-
// succeed for one of them (affected rows = 0 for the loser).
|
|
444
|
+
// The UPDATE with `uses < max_uses` clause ensures that concurrent joins
|
|
445
|
+
// only succeed up to max_uses times (affected rows = 0 for the losers).
|
|
407
446
|
const userId = `usr_${randomUUID().replace(/-/g, '')}`
|
|
408
|
-
const passwordHash =
|
|
447
|
+
const passwordHash = await hashPasswordAsync(password)
|
|
409
448
|
const now = new Date().toISOString()
|
|
410
449
|
|
|
411
450
|
try {
|
|
412
451
|
db.transaction(() => {
|
|
413
|
-
// Re-check invitation state inside the transaction (may have changed)
|
|
452
|
+
// Re-check invitation state inside the transaction (may have changed).
|
|
453
|
+
// Only check uses >= max_uses — consumed_at is set conditionally below.
|
|
414
454
|
const currentInv = db
|
|
415
|
-
.query('SELECT
|
|
416
|
-
.get({ $hash: tokenHash }) as
|
|
417
|
-
|
|
418
|
-
if (
|
|
419
|
-
!currentInv ||
|
|
420
|
-
currentInv.consumed_at ||
|
|
421
|
-
currentInv.uses >= currentInv.max_uses
|
|
422
|
-
) {
|
|
455
|
+
.query('SELECT uses, max_uses FROM invitations WHERE token_hash = $hash')
|
|
456
|
+
.get({ $hash: tokenHash }) as { uses: number; max_uses: number } | null
|
|
457
|
+
|
|
458
|
+
if (!currentInv || currentInv.uses >= currentInv.max_uses) {
|
|
423
459
|
throw new Error('invitation_used')
|
|
424
460
|
}
|
|
425
461
|
|
|
426
|
-
// Insert user
|
|
427
|
-
|
|
428
|
-
`INSERT INTO users (id, username, password_hash, role, created_at)
|
|
429
|
-
VALUES ($id, $username, $passwordHash, $role, $now)`,
|
|
430
|
-
).run({
|
|
431
|
-
$id: userId,
|
|
432
|
-
$username: username,
|
|
433
|
-
$passwordHash: passwordHash,
|
|
434
|
-
$role: invitation.role,
|
|
435
|
-
$now: now,
|
|
436
|
-
})
|
|
437
|
-
|
|
438
|
-
// Atomically increment uses — will fail (changes=0) if race condition
|
|
462
|
+
// Insert user — INSERT OR IGNORE handles the UNIQUE username race:
|
|
463
|
+
// if another concurrent request inserted the same username, changes=0.
|
|
439
464
|
const result = db
|
|
465
|
+
.query(
|
|
466
|
+
`INSERT OR IGNORE INTO users (id, username, password_hash, role, created_at)
|
|
467
|
+
VALUES ($id, $username, $passwordHash, $role, $now)`,
|
|
468
|
+
)
|
|
469
|
+
.run({
|
|
470
|
+
$id: userId,
|
|
471
|
+
$username: username,
|
|
472
|
+
$passwordHash: passwordHash,
|
|
473
|
+
$role: invitation.role,
|
|
474
|
+
$now: now,
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
if (result.changes === 0) {
|
|
478
|
+
throw new Error('username_taken')
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Atomically increment uses — will fail (changes=0) if race condition.
|
|
482
|
+
// consumed_at is only set when the last slot is used (uses + 1 >= max_uses).
|
|
483
|
+
const updateResult = db
|
|
440
484
|
.query(
|
|
441
485
|
`UPDATE invitations
|
|
442
|
-
SET uses = uses + 1,
|
|
486
|
+
SET uses = uses + 1,
|
|
487
|
+
consumed_at = CASE WHEN uses + 1 >= max_uses THEN $now ELSE consumed_at END,
|
|
488
|
+
consumed_by = CASE WHEN uses + 1 >= max_uses THEN $userId ELSE consumed_by END
|
|
443
489
|
WHERE token_hash = $hash
|
|
444
|
-
AND uses < max_uses
|
|
445
|
-
AND consumed_at IS NULL`,
|
|
490
|
+
AND uses < max_uses`,
|
|
446
491
|
)
|
|
447
492
|
.run({ $now: now, $userId: userId, $hash: tokenHash })
|
|
448
493
|
|
|
449
|
-
if (
|
|
494
|
+
if (updateResult.changes === 0) {
|
|
450
495
|
throw new Error('invitation_used')
|
|
451
496
|
}
|
|
452
497
|
|
|
@@ -468,11 +513,14 @@ app.post('/join', async c => {
|
|
|
468
513
|
if (err instanceof Error && err.message === 'invitation_used') {
|
|
469
514
|
return c.json({ error: 'Invitation already used or invalid' }, 400)
|
|
470
515
|
}
|
|
516
|
+
if (err instanceof Error && err.message === 'username_taken') {
|
|
517
|
+
return c.json({ error: 'Username already taken' }, 409)
|
|
518
|
+
}
|
|
471
519
|
console.warn('[/join] transaction failed:', err)
|
|
472
520
|
return c.json({ error: 'Failed to process invitation' }, 500)
|
|
473
521
|
}
|
|
474
522
|
|
|
475
|
-
const isHttps = c
|
|
523
|
+
const isHttps = isSecureRequest(c)
|
|
476
524
|
return issueAndRespond(c, userId, isHttps)
|
|
477
525
|
})
|
|
478
526
|
|
|
@@ -621,13 +669,4 @@ app.get(
|
|
|
621
669
|
},
|
|
622
670
|
)
|
|
623
671
|
|
|
624
|
-
function parseCookieValue(
|
|
625
|
-
cookieHeader: string | undefined,
|
|
626
|
-
name: string,
|
|
627
|
-
): string | undefined {
|
|
628
|
-
if (!cookieHeader) return undefined
|
|
629
|
-
const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${name}=([^;]+)`))
|
|
630
|
-
return match ? match[1] : undefined
|
|
631
|
-
}
|
|
632
|
-
|
|
633
672
|
export default app
|
|
@@ -1,30 +1,23 @@
|
|
|
1
1
|
import { Hono } from 'hono'
|
|
2
|
-
import { storeBindSession } from '../../store'
|
|
3
|
-
import {
|
|
4
|
-
resolveExistingWebSessionId,
|
|
5
|
-
toWebSessionId,
|
|
6
|
-
} from '../../services/session'
|
|
7
2
|
|
|
8
3
|
const app = new Hono()
|
|
9
4
|
|
|
10
|
-
/**
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
storeBindSession(resolvedSessionId, uuid)
|
|
27
|
-
return c.json({ ok: true, sessionId: toWebSessionId(resolvedSessionId) })
|
|
5
|
+
/**
|
|
6
|
+
* POST /web/bind — REMOVED (security: unauthenticated session binding).
|
|
7
|
+
*
|
|
8
|
+
* Returns 410 Gone to signal clients that this endpoint has been
|
|
9
|
+
* intentionally deprecated and will not be restored.
|
|
10
|
+
*/
|
|
11
|
+
app.all('/bind', c => {
|
|
12
|
+
return c.json(
|
|
13
|
+
{
|
|
14
|
+
error: {
|
|
15
|
+
type: 'gone',
|
|
16
|
+
message: '/web/bind has been removed. Use authenticated session creation instead.',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
410,
|
|
20
|
+
)
|
|
28
21
|
})
|
|
29
22
|
|
|
30
23
|
export default app
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { log, error as logError } from '../../logger'
|
|
2
2
|
import { Hono } from 'hono'
|
|
3
3
|
import { sessionAuth } from '../../auth/middleware'
|
|
4
|
+
import { SYSTEM_USER_ID } from '../../auth/constants'
|
|
4
5
|
import { getAutomationStateSnapshot } from '../../services/automationState'
|
|
5
6
|
import {
|
|
6
7
|
archiveSession,
|
|
@@ -25,9 +26,29 @@ import { getEventBus } from '../../transport/event-bus'
|
|
|
25
26
|
|
|
26
27
|
const app = new Hono()
|
|
27
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Resolve the user ID to bind a session to.
|
|
31
|
+
* When the request comes via API key (userId = __system__), check the
|
|
32
|
+
* X-User-Id header for a real user identity. If no valid identity is
|
|
33
|
+
* found, returns null — the session will be orphaned (admin-visible).
|
|
34
|
+
*/
|
|
35
|
+
function resolveBindUserId(
|
|
36
|
+
c: import('hono').Context,
|
|
37
|
+
): string | null {
|
|
38
|
+
const uuid = c.get('userId')!
|
|
39
|
+
if (uuid !== SYSTEM_USER_ID) return uuid
|
|
40
|
+
|
|
41
|
+
// API key auth — check X-User-Id header for real user identity
|
|
42
|
+
const headerUserId = c.req.header('X-User-Id')
|
|
43
|
+
if (headerUserId && headerUserId !== SYSTEM_USER_ID) {
|
|
44
|
+
return headerUserId
|
|
45
|
+
}
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
|
|
28
49
|
/** POST /web/sessions — Create a session from web UI */
|
|
29
50
|
app.post('/sessions', sessionAuth, async c => {
|
|
30
|
-
const
|
|
51
|
+
const bindUserId = resolveBindUserId(c)
|
|
31
52
|
const body = await c.req.json()
|
|
32
53
|
|
|
33
54
|
// Pre-validate BEFORE creating the session record.
|
|
@@ -52,7 +73,9 @@ app.post('/sessions', sessionAuth, async c => {
|
|
|
52
73
|
title: body.title || env.machineName || 'ACP Agent',
|
|
53
74
|
source: 'acp',
|
|
54
75
|
}).id
|
|
55
|
-
|
|
76
|
+
if (bindUserId) {
|
|
77
|
+
storeBindSession(sessionId, bindUserId)
|
|
78
|
+
}
|
|
56
79
|
const session = getSession(sessionId)
|
|
57
80
|
return c.json(toWebSessionResponse(session!), 200)
|
|
58
81
|
}
|
|
@@ -65,8 +88,10 @@ app.post('/sessions', sessionAuth, async c => {
|
|
|
65
88
|
permission_mode: body.permission_mode || 'default',
|
|
66
89
|
})
|
|
67
90
|
|
|
68
|
-
// Auto-bind to creator's UUID
|
|
69
|
-
|
|
91
|
+
// Auto-bind to creator's UUID (skip for system/API key without real user)
|
|
92
|
+
if (bindUserId) {
|
|
93
|
+
storeBindSession(session.id, bindUserId)
|
|
94
|
+
}
|
|
70
95
|
|
|
71
96
|
// Dispatch work to environment if specified
|
|
72
97
|
if (body.environment_id) {
|
|
@@ -240,7 +265,7 @@ app.get('/sessions/:id/events', sessionAuth, async c => {
|
|
|
240
265
|
* and kills the child process, releasing capacity. */
|
|
241
266
|
app.delete('/sessions/:id', sessionAuth, async c => {
|
|
242
267
|
const uuid = c.get('userId')!
|
|
243
|
-
const sessionId = resolveOwnedWebSessionId(c.req.param('id')!, uuid)
|
|
268
|
+
const sessionId = resolveOwnedWebSessionId(c.req.param('id')!, uuid, true)
|
|
244
269
|
if (!sessionId) {
|
|
245
270
|
return c.json(
|
|
246
271
|
{ error: { type: 'forbidden', message: 'Not your session' } },
|