@saluzi/saluzi-edu 0.2.11 → 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 +216 -142
- package/dist/oms-transition.js +2 -1
- 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-DNdfPOYy.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-CP3Z6ckr.js → ShareLanding-DUZrreJ7.js} +1 -1
- package/dist/rcs-web/assets/TeamCreate-B8ZV6EdY.js +1 -0
- package/dist/rcs-web/assets/{TeamDetail-CkNTFoMB.js → TeamDetail-DBAsJ__i.js} +2 -2
- package/dist/rcs-web/assets/{TeamList-Bf0sQ5RQ.js → TeamList-C-Hte4nV.js} +1 -1
- package/dist/rcs-web/assets/TeamSettings-CNVwYwcp.js +1 -0
- package/dist/rcs-web/assets/UserSettings-DRdz9kYE.js +1 -0
- package/dist/rcs-web/assets/{arc-BxxmNQO6.js → arc-CJxirXEQ.js} +1 -1
- package/dist/rcs-web/assets/{architectureDiagram-3BPJPVTR-DBjkRdbd.js → architectureDiagram-3BPJPVTR-BZvIHrLA.js} +1 -1
- package/dist/rcs-web/assets/{blockDiagram-GPEHLZMM-BF6COWlM.js → blockDiagram-GPEHLZMM-3p0y3Ija.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/c4Diagram-AAUBKEIU-DHOf390v.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-CeJN8yjM.js → chunk-2J33WTMH-B8gQz22j.js} +1 -1
- package/dist/rcs-web/assets/{chunk-4BX2VUAB-jP76_P2A.js → chunk-4BX2VUAB-CgLEZsTU.js} +1 -1
- package/dist/rcs-web/assets/{chunk-55IACEB6-BsFA6emB.js → chunk-55IACEB6-0mdE_-IS.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/chunk-727SXJPM-BF4HrQjZ.js → dist/rcs-web/assets/chunk-727SXJPM-DdtjnIHR.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/chunk-AQP2D5EJ-DToJ74Kc.js → dist/rcs-web/assets/chunk-AQP2D5EJ-BtwFPk50.js} +1 -1
- package/dist/rcs-web/assets/{chunk-FMBD7UC4-DVTARgsa.js → chunk-FMBD7UC4-BcO9mQ0P.js} +1 -1
- package/dist/rcs-web/assets/{chunk-ND2GUHAM-C50Gqv9c.js → chunk-ND2GUHAM-BvRq13S_.js} +1 -1
- package/dist/rcs-web/assets/{chunk-QZHKN3VN-EsIIh6zV.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-BnufHNTT.js → code-block-IT6T5CEO-B1p-vO-3.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/cose-bilkent-S5V4N54A-M6tNt14I.js → dist/rcs-web/assets/cose-bilkent-S5V4N54A-L0rUZuH5.js} +1 -1
- package/dist/rcs-web/assets/{dagre-BM42HDAG-DaTm29qG.js → dagre-BM42HDAG-CHGuio22.js} +1 -1
- package/dist/rcs-web/assets/{diagram-2AECGRRQ-fqoeOvq2.js → diagram-2AECGRRQ-DIDRyQk8.js} +1 -1
- package/dist/rcs-web/assets/{diagram-5GNKFQAL-6s2CiaGG.js → diagram-5GNKFQAL-CNubKb6P.js} +1 -1
- package/dist/rcs-web/assets/{diagram-KO2AKTUF-C0aw_N95.js → diagram-KO2AKTUF-CzQh664-.js} +1 -1
- package/dist/rcs-web/assets/{diagram-LMA3HP47-C8_Rv3rB.js → diagram-LMA3HP47-CWBR7-VG.js} +1 -1
- package/dist/rcs-web/assets/{diagram-OG6HWLK6-Dae4u8Uc.js → diagram-OG6HWLK6-BMHdlbcZ.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/erDiagram-TEJ5UH35-CosS5VWr.js → dist/rcs-web/assets/erDiagram-TEJ5UH35-OgT8dC-K.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/flowDiagram-I6XJVG4X-D_Mxs46-.js → dist/rcs-web/assets/flowDiagram-I6XJVG4X-DRrCY2oB.js} +1 -1
- package/dist/rcs-web/assets/{ganttDiagram-6RSMTGT7-B0jy8Am_.js → ganttDiagram-6RSMTGT7-DctpzEKN.js} +1 -1
- package/dist/rcs-web/assets/{gitGraphDiagram-PVQCEYII-CSdu2HVI.js → gitGraphDiagram-PVQCEYII-5KpLszm9.js} +1 -1
- package/dist/rcs-web/assets/{infoDiagram-5YYISTIA-Dcbc9L1S.js → infoDiagram-5YYISTIA-BAKD_Tt7.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/ishikawaDiagram-YF4QCWOH-BLMjFE4t.js → dist/rcs-web/assets/ishikawaDiagram-YF4QCWOH-VHOPG4Nc.js} +1 -1
- package/dist/rcs-web/assets/{journeyDiagram-JHISSGLW-Dw4nh1xI.js → journeyDiagram-JHISSGLW-BtGih-ik.js} +1 -1
- package/dist/rcs-web/assets/{kanban-definition-UN3LZRKU-DkU3j_ly.js → kanban-definition-UN3LZRKU-BsEGZsD1.js} +1 -1
- package/dist/rcs-web/assets/{linear-N5Cify7M.js → linear-Dj636PNG.js} +1 -1
- package/dist/rcs-web/assets/main-CkqtCllF.js +238 -0
- package/dist/rcs-web/assets/{mermaid-VLURNSYL-gfiJUpl_.js → mermaid-VLURNSYL-DutpHHm9.js} +4 -4
- package/dist/rcs-web/assets/{mermaid.core-B_Xc65Bf.js → mermaid.core-XHHf0pen.js} +5 -5
- package/dist/rcs-web/assets/{mindmap-definition-RKZ34NQL-DUavHfNu.js → mindmap-definition-RKZ34NQL-BBa8V_3g.js} +1 -1
- package/dist/rcs-web/assets/{pieDiagram-4H26LBE5-DSJaToTU.js → pieDiagram-4H26LBE5-BuNEdo9H.js} +1 -1
- package/dist/rcs-web/assets/{quadrantDiagram-W4KKPZXB-BFp6TnHj.js → quadrantDiagram-W4KKPZXB-CsJgyQIN.js} +1 -1
- package/dist/rcs-web/assets/{requirementDiagram-4Y6WPE33-eil6F3-O.js → requirementDiagram-4Y6WPE33-DqMJZ914.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/sankeyDiagram-5OEKKPKP-DbGhoepK.js → dist/rcs-web/assets/sankeyDiagram-5OEKKPKP-CVsz09q2.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/sequenceDiagram-3UESZ5HK-B0YrN1ud.js → dist/rcs-web/assets/sequenceDiagram-3UESZ5HK-BRp4Wddn.js} +1 -1
- package/dist/rcs-web/assets/{stateDiagram-AJRCARHV-CofFPj3N.js → stateDiagram-AJRCARHV-S-QqQr-D.js} +1 -1
- package/dist/rcs-web/assets/{stateDiagram-v2-BHNVJYJU-DPlcvPn1.js → stateDiagram-v2-BHNVJYJU-D77KfDr-.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/timeline-definition-PNZ67QCA-Bx4dusy8.js → dist/rcs-web/assets/timeline-definition-PNZ67QCA-De8bFWnE.js} +1 -1
- package/dist/rcs-web/assets/{useTeams-MDI4djXg.js → useTeams-CqJOqEy1.js} +1 -1
- package/dist/rcs-web/assets/{vennDiagram-CIIHVFJN-B_-3l-MF.js → vennDiagram-CIIHVFJN-Dikwrn1L.js} +1 -1
- package/dist/rcs-web/assets/{wardleyDiagram-YWT4CUSO-DNSn-pGp.js → wardleyDiagram-YWT4CUSO-DUzpiV6H.js} +1 -1
- package/dist/rcs-web/assets/{workerType-OM09y7Lk.js → workerType-DOQ0AHrq.js} +1 -1
- package/dist/rcs-web/assets/{xychartDiagram-2RQKCTM6-TqUbNp0j.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__/auth-security.test.ts +18 -5
- 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 +116 -78
- 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-DNdfPOYy.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-CP3Z6ckr.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-CkNTFoMB.js → TeamDetail-DBAsJ__i.js} +2 -2
- package/packages/remote-control-server/web/dist/assets/{TeamList-Bf0sQ5RQ.js → TeamList-C-Hte4nV.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/TeamSettings-CNVwYwcp.js +1 -0
- package/packages/remote-control-server/web/dist/assets/UserSettings-DRdz9kYE.js +1 -0
- package/packages/remote-control-server/web/dist/assets/{arc-BxxmNQO6.js → arc-CJxirXEQ.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{architectureDiagram-3BPJPVTR-DBjkRdbd.js → architectureDiagram-3BPJPVTR-BZvIHrLA.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{blockDiagram-GPEHLZMM-BF6COWlM.js → blockDiagram-GPEHLZMM-3p0y3Ija.js} +1 -1
- package/{dist/rcs-web/assets/c4Diagram-AAUBKEIU-DHOf390v.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-CeJN8yjM.js → chunk-2J33WTMH-B8gQz22j.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-4BX2VUAB-jP76_P2A.js → chunk-4BX2VUAB-CgLEZsTU.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-55IACEB6-BsFA6emB.js → chunk-55IACEB6-0mdE_-IS.js} +1 -1
- package/{dist/rcs-web/assets/chunk-727SXJPM-BF4HrQjZ.js → packages/remote-control-server/web/dist/assets/chunk-727SXJPM-DdtjnIHR.js} +1 -1
- package/{dist/rcs-web/assets/chunk-AQP2D5EJ-DToJ74Kc.js → packages/remote-control-server/web/dist/assets/chunk-AQP2D5EJ-BtwFPk50.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-FMBD7UC4-DVTARgsa.js → chunk-FMBD7UC4-BcO9mQ0P.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-ND2GUHAM-C50Gqv9c.js → chunk-ND2GUHAM-BvRq13S_.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-QZHKN3VN-EsIIh6zV.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-BnufHNTT.js → code-block-IT6T5CEO-B1p-vO-3.js} +1 -1
- package/{dist/rcs-web/assets/cose-bilkent-S5V4N54A-M6tNt14I.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-DaTm29qG.js → dagre-BM42HDAG-CHGuio22.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{diagram-2AECGRRQ-fqoeOvq2.js → diagram-2AECGRRQ-DIDRyQk8.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{diagram-5GNKFQAL-6s2CiaGG.js → diagram-5GNKFQAL-CNubKb6P.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{diagram-KO2AKTUF-C0aw_N95.js → diagram-KO2AKTUF-CzQh664-.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{diagram-LMA3HP47-C8_Rv3rB.js → diagram-LMA3HP47-CWBR7-VG.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{diagram-OG6HWLK6-Dae4u8Uc.js → diagram-OG6HWLK6-BMHdlbcZ.js} +1 -1
- package/{dist/rcs-web/assets/erDiagram-TEJ5UH35-CosS5VWr.js → packages/remote-control-server/web/dist/assets/erDiagram-TEJ5UH35-OgT8dC-K.js} +1 -1
- package/{dist/rcs-web/assets/flowDiagram-I6XJVG4X-D_Mxs46-.js → packages/remote-control-server/web/dist/assets/flowDiagram-I6XJVG4X-DRrCY2oB.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{ganttDiagram-6RSMTGT7-B0jy8Am_.js → ganttDiagram-6RSMTGT7-DctpzEKN.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{gitGraphDiagram-PVQCEYII-CSdu2HVI.js → gitGraphDiagram-PVQCEYII-5KpLszm9.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{infoDiagram-5YYISTIA-Dcbc9L1S.js → infoDiagram-5YYISTIA-BAKD_Tt7.js} +1 -1
- package/{dist/rcs-web/assets/ishikawaDiagram-YF4QCWOH-BLMjFE4t.js → packages/remote-control-server/web/dist/assets/ishikawaDiagram-YF4QCWOH-VHOPG4Nc.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{journeyDiagram-JHISSGLW-Dw4nh1xI.js → journeyDiagram-JHISSGLW-BtGih-ik.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{kanban-definition-UN3LZRKU-DkU3j_ly.js → kanban-definition-UN3LZRKU-BsEGZsD1.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{linear-N5Cify7M.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-gfiJUpl_.js → mermaid-VLURNSYL-DutpHHm9.js} +4 -4
- package/packages/remote-control-server/web/dist/assets/{mermaid.core-B_Xc65Bf.js → mermaid.core-XHHf0pen.js} +5 -5
- package/packages/remote-control-server/web/dist/assets/{mindmap-definition-RKZ34NQL-DUavHfNu.js → mindmap-definition-RKZ34NQL-BBa8V_3g.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{pieDiagram-4H26LBE5-DSJaToTU.js → pieDiagram-4H26LBE5-BuNEdo9H.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{quadrantDiagram-W4KKPZXB-BFp6TnHj.js → quadrantDiagram-W4KKPZXB-CsJgyQIN.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{requirementDiagram-4Y6WPE33-eil6F3-O.js → requirementDiagram-4Y6WPE33-DqMJZ914.js} +1 -1
- package/{dist/rcs-web/assets/sankeyDiagram-5OEKKPKP-DbGhoepK.js → packages/remote-control-server/web/dist/assets/sankeyDiagram-5OEKKPKP-CVsz09q2.js} +1 -1
- package/{dist/rcs-web/assets/sequenceDiagram-3UESZ5HK-B0YrN1ud.js → packages/remote-control-server/web/dist/assets/sequenceDiagram-3UESZ5HK-BRp4Wddn.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{stateDiagram-AJRCARHV-CofFPj3N.js → stateDiagram-AJRCARHV-S-QqQr-D.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{stateDiagram-v2-BHNVJYJU-DPlcvPn1.js → stateDiagram-v2-BHNVJYJU-D77KfDr-.js} +1 -1
- package/{dist/rcs-web/assets/timeline-definition-PNZ67QCA-Bx4dusy8.js → packages/remote-control-server/web/dist/assets/timeline-definition-PNZ67QCA-De8bFWnE.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{useTeams-MDI4djXg.js → useTeams-CqJOqEy1.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{vennDiagram-CIIHVFJN-B_-3l-MF.js → vennDiagram-CIIHVFJN-Dikwrn1L.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{wardleyDiagram-YWT4CUSO-DNSn-pGp.js → wardleyDiagram-YWT4CUSO-DUzpiV6H.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{workerType-OM09y7Lk.js → workerType-DOQ0AHrq.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{xychartDiagram-2RQKCTM6-TqUbNp0j.js → xychartDiagram-2RQKCTM6-OZNbB32D.js} +1 -1
- package/packages/remote-control-server/web/dist/index.html +1 -1
- package/dist/rcs-web/assets/Dashboard-BwotBWlJ.js +0 -1
- package/dist/rcs-web/assets/Join-4yrksbdX.js +0 -1
- package/dist/rcs-web/assets/Login-Cxm_yEeE.js +0 -1
- package/dist/rcs-web/assets/Setup-BiAAxugt.js +0 -1
- package/dist/rcs-web/assets/TeamCreate-DgdCwRSb.js +0 -1
- package/dist/rcs-web/assets/TeamSettings-BaNAfXU3.js +0 -1
- package/dist/rcs-web/assets/UserSettings-CsutQjxG.js +0 -1
- package/dist/rcs-web/assets/channel-JsvbMQjW.js +0 -1
- package/dist/rcs-web/assets/classDiagram-4FO5ZUOK-BXn8Uc4c.js +0 -1
- package/dist/rcs-web/assets/classDiagram-v2-Q7XG4LA2-BXn8Uc4c.js +0 -1
- package/dist/rcs-web/assets/i18n-Dbt-vEQ3.js +0 -1
- package/dist/rcs-web/assets/main-30YqUvTd.js +0 -238
- package/packages/remote-control-server/web/dist/assets/Dashboard-BwotBWlJ.js +0 -1
- package/packages/remote-control-server/web/dist/assets/Join-4yrksbdX.js +0 -1
- package/packages/remote-control-server/web/dist/assets/Login-Cxm_yEeE.js +0 -1
- package/packages/remote-control-server/web/dist/assets/Setup-BiAAxugt.js +0 -1
- package/packages/remote-control-server/web/dist/assets/TeamCreate-DgdCwRSb.js +0 -1
- package/packages/remote-control-server/web/dist/assets/TeamSettings-BaNAfXU3.js +0 -1
- package/packages/remote-control-server/web/dist/assets/UserSettings-CsutQjxG.js +0 -1
- package/packages/remote-control-server/web/dist/assets/channel-JsvbMQjW.js +0 -1
- package/packages/remote-control-server/web/dist/assets/classDiagram-4FO5ZUOK-BXn8Uc4c.js +0 -1
- package/packages/remote-control-server/web/dist/assets/classDiagram-v2-Q7XG4LA2-BXn8Uc4c.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-30YqUvTd.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,8 +1,13 @@
|
|
|
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
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
hashPasswordAsync,
|
|
8
|
+
verifyPassword,
|
|
9
|
+
verifyPasswordAsync,
|
|
10
|
+
} from '../../auth/password'
|
|
6
11
|
import {
|
|
7
12
|
issueSessionToken,
|
|
8
13
|
resolveSessionToken,
|
|
@@ -14,6 +19,25 @@ import { SYSTEM_USER_ID } from '../../auth/constants'
|
|
|
14
19
|
|
|
15
20
|
const app = new Hono()
|
|
16
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
|
+
|
|
17
41
|
// ---------------------------------------------------------------------------
|
|
18
42
|
// Login rate limiting (Task 2) — in-memory, per IP+username
|
|
19
43
|
// ---------------------------------------------------------------------------
|
|
@@ -171,42 +195,36 @@ app.post('/setup', async c => {
|
|
|
171
195
|
|
|
172
196
|
const db = getDb()
|
|
173
197
|
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
198
|
+
// Atomic INSERT with WHERE NOT EXISTS guard — race-free under SQLite's
|
|
199
|
+
// serial write model. The first request INSERTs (changes=1), the second
|
|
200
|
+
// finds the admin row already present (changes=0 → 409).
|
|
177
201
|
const userId = `usr_${randomUUID().replace(/-/g, '')}`
|
|
178
|
-
const passwordHash =
|
|
202
|
+
const passwordHash = await hashPasswordAsync(password)
|
|
179
203
|
const now = new Date().toISOString()
|
|
180
204
|
|
|
181
205
|
try {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
count: number
|
|
185
|
-
}
|
|
186
|
-
if (count.count > 0) {
|
|
187
|
-
throw new Error('setup_completed')
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
db.query(
|
|
206
|
+
const result = db
|
|
207
|
+
.query(
|
|
191
208
|
`INSERT INTO users (id, username, password_hash, role, created_at)
|
|
192
|
-
|
|
193
|
-
|
|
209
|
+
SELECT $id, $username, $passwordHash, 'admin', $now
|
|
210
|
+
WHERE NOT EXISTS (SELECT 1 FROM users WHERE role = 'admin')`,
|
|
211
|
+
)
|
|
212
|
+
.run({
|
|
194
213
|
$id: userId,
|
|
195
214
|
$username: username,
|
|
196
215
|
$passwordHash: passwordHash,
|
|
197
|
-
$role: 'admin',
|
|
198
216
|
$now: now,
|
|
199
217
|
})
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (err instanceof Error && err.message === 'setup_completed') {
|
|
218
|
+
|
|
219
|
+
if (result.changes === 0) {
|
|
203
220
|
return c.json({ error: 'Setup already completed' }, 409)
|
|
204
221
|
}
|
|
205
|
-
|
|
222
|
+
} catch (err) {
|
|
223
|
+
console.warn('[/setup] insert failed:', err)
|
|
206
224
|
return c.json({ error: 'Setup failed' }, 500)
|
|
207
225
|
}
|
|
208
226
|
|
|
209
|
-
const isHttps = c
|
|
227
|
+
const isHttps = isSecureRequest(c)
|
|
210
228
|
return issueAndRespond(c, userId, isHttps)
|
|
211
229
|
})
|
|
212
230
|
|
|
@@ -242,8 +260,16 @@ app.post('/login', async c => {
|
|
|
242
260
|
.query('SELECT * FROM users WHERE username = $username')
|
|
243
261
|
.get({ $username: username }) as UserRow | null
|
|
244
262
|
|
|
245
|
-
// Generic error to avoid leaking user existence
|
|
246
|
-
|
|
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)) {
|
|
247
273
|
recordFailedAttempt(clientIp, username)
|
|
248
274
|
return c.json({ error: 'Invalid credentials' }, 401)
|
|
249
275
|
}
|
|
@@ -257,7 +283,7 @@ app.post('/login', async c => {
|
|
|
257
283
|
$id: user.id,
|
|
258
284
|
})
|
|
259
285
|
|
|
260
|
-
const isHttps = c
|
|
286
|
+
const isHttps = isSecureRequest(c)
|
|
261
287
|
return issueAndRespond(c, user.id, isHttps)
|
|
262
288
|
})
|
|
263
289
|
|
|
@@ -274,7 +300,7 @@ app.post('/refresh', async c => {
|
|
|
274
300
|
const refreshToken =
|
|
275
301
|
(body.refreshToken as string) ||
|
|
276
302
|
(body.refresh_token as string) ||
|
|
277
|
-
|
|
303
|
+
getCookie(c, 'rcs_refresh')
|
|
278
304
|
|
|
279
305
|
if (!refreshToken) {
|
|
280
306
|
return c.json({ error: 'Refresh token required' }, 401)
|
|
@@ -291,7 +317,7 @@ app.post('/refresh', async c => {
|
|
|
291
317
|
revokeSessionToken(refreshToken, db)
|
|
292
318
|
|
|
293
319
|
// Issue new access + refresh token pair
|
|
294
|
-
const isHttps = c
|
|
320
|
+
const isHttps = isSecureRequest(c)
|
|
295
321
|
const { accessToken, refreshToken: newRefreshToken } = issueSessionToken(
|
|
296
322
|
userId,
|
|
297
323
|
db,
|
|
@@ -318,7 +344,7 @@ app.post('/logout', async c => {
|
|
|
318
344
|
? authHeader.slice('Bearer '.length)
|
|
319
345
|
: undefined
|
|
320
346
|
|
|
321
|
-
const cookieToken =
|
|
347
|
+
const cookieToken = getCookie(c, 'rcs_access')
|
|
322
348
|
|
|
323
349
|
const token = bearerToken || cookieToken
|
|
324
350
|
|
|
@@ -327,7 +353,18 @@ app.post('/logout', async c => {
|
|
|
327
353
|
const db = getDb()
|
|
328
354
|
revokeSessionToken(token, db)
|
|
329
355
|
} catch (err) {
|
|
330
|
-
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
|
|
331
368
|
}
|
|
332
369
|
}
|
|
333
370
|
|
|
@@ -388,66 +425,73 @@ app.post('/join', async c => {
|
|
|
388
425
|
return c.json({ error: 'Invitation expired' }, 400)
|
|
389
426
|
}
|
|
390
427
|
|
|
391
|
-
// Check if already fully consumed (pre-check; atomic check below)
|
|
392
|
-
|
|
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) {
|
|
393
432
|
return c.json({ error: 'Invitation already used' }, 400)
|
|
394
433
|
}
|
|
395
434
|
|
|
396
|
-
// Check username uniqueness
|
|
435
|
+
// Check username uniqueness (pre-check; atomic check inside transaction)
|
|
397
436
|
const existingUser = db
|
|
398
437
|
.query('SELECT id FROM users WHERE username = $username')
|
|
399
438
|
.get({ $username: username })
|
|
400
439
|
if (existingUser) {
|
|
401
|
-
return c.json({ error: 'Username already taken' },
|
|
440
|
+
return c.json({ error: 'Username already taken' }, 409)
|
|
402
441
|
}
|
|
403
442
|
|
|
404
443
|
// Create user + atomically consume invitation slot in a transaction.
|
|
405
|
-
// The UPDATE with `uses < max_uses
|
|
406
|
-
//
|
|
407
|
-
// 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).
|
|
408
446
|
const userId = `usr_${randomUUID().replace(/-/g, '')}`
|
|
409
|
-
const passwordHash =
|
|
447
|
+
const passwordHash = await hashPasswordAsync(password)
|
|
410
448
|
const now = new Date().toISOString()
|
|
411
449
|
|
|
412
450
|
try {
|
|
413
451
|
db.transaction(() => {
|
|
414
|
-
// 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.
|
|
415
454
|
const currentInv = db
|
|
416
|
-
.query('SELECT
|
|
417
|
-
.get({ $hash: tokenHash }) as
|
|
418
|
-
|
|
419
|
-
if (
|
|
420
|
-
!currentInv ||
|
|
421
|
-
currentInv.consumed_at ||
|
|
422
|
-
currentInv.uses >= currentInv.max_uses
|
|
423
|
-
) {
|
|
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) {
|
|
424
459
|
throw new Error('invitation_used')
|
|
425
460
|
}
|
|
426
461
|
|
|
427
|
-
// Insert user
|
|
428
|
-
|
|
429
|
-
`INSERT INTO users (id, username, password_hash, role, created_at)
|
|
430
|
-
VALUES ($id, $username, $passwordHash, $role, $now)`,
|
|
431
|
-
).run({
|
|
432
|
-
$id: userId,
|
|
433
|
-
$username: username,
|
|
434
|
-
$passwordHash: passwordHash,
|
|
435
|
-
$role: invitation.role,
|
|
436
|
-
$now: now,
|
|
437
|
-
})
|
|
438
|
-
|
|
439
|
-
// 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.
|
|
440
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
|
|
441
484
|
.query(
|
|
442
485
|
`UPDATE invitations
|
|
443
|
-
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
|
|
444
489
|
WHERE token_hash = $hash
|
|
445
|
-
AND uses < max_uses
|
|
446
|
-
AND consumed_at IS NULL`,
|
|
490
|
+
AND uses < max_uses`,
|
|
447
491
|
)
|
|
448
492
|
.run({ $now: now, $userId: userId, $hash: tokenHash })
|
|
449
493
|
|
|
450
|
-
if (
|
|
494
|
+
if (updateResult.changes === 0) {
|
|
451
495
|
throw new Error('invitation_used')
|
|
452
496
|
}
|
|
453
497
|
|
|
@@ -469,11 +513,14 @@ app.post('/join', async c => {
|
|
|
469
513
|
if (err instanceof Error && err.message === 'invitation_used') {
|
|
470
514
|
return c.json({ error: 'Invitation already used or invalid' }, 400)
|
|
471
515
|
}
|
|
516
|
+
if (err instanceof Error && err.message === 'username_taken') {
|
|
517
|
+
return c.json({ error: 'Username already taken' }, 409)
|
|
518
|
+
}
|
|
472
519
|
console.warn('[/join] transaction failed:', err)
|
|
473
520
|
return c.json({ error: 'Failed to process invitation' }, 500)
|
|
474
521
|
}
|
|
475
522
|
|
|
476
|
-
const isHttps = c
|
|
523
|
+
const isHttps = isSecureRequest(c)
|
|
477
524
|
return issueAndRespond(c, userId, isHttps)
|
|
478
525
|
})
|
|
479
526
|
|
|
@@ -525,10 +572,7 @@ app.post(
|
|
|
525
572
|
}
|
|
526
573
|
|
|
527
574
|
if (newPassword.length < 8) {
|
|
528
|
-
return c.json(
|
|
529
|
-
{ error: 'Password must be at least 8 characters' },
|
|
530
|
-
400,
|
|
531
|
-
)
|
|
575
|
+
return c.json({ error: 'Password must be at least 8 characters' }, 400)
|
|
532
576
|
}
|
|
533
577
|
|
|
534
578
|
const db = getDb()
|
|
@@ -540,7 +584,10 @@ app.post(
|
|
|
540
584
|
return c.json({ error: 'User not found' }, 404)
|
|
541
585
|
}
|
|
542
586
|
|
|
543
|
-
const isValid = await verifyPasswordAsync(
|
|
587
|
+
const isValid = await verifyPasswordAsync(
|
|
588
|
+
currentPassword,
|
|
589
|
+
user.password_hash,
|
|
590
|
+
)
|
|
544
591
|
if (!isValid) {
|
|
545
592
|
return c.json({ error: 'Current password is incorrect' }, 401)
|
|
546
593
|
}
|
|
@@ -622,13 +669,4 @@ app.get(
|
|
|
622
669
|
},
|
|
623
670
|
)
|
|
624
671
|
|
|
625
|
-
function parseCookieValue(
|
|
626
|
-
cookieHeader: string | undefined,
|
|
627
|
-
name: string,
|
|
628
|
-
): string | undefined {
|
|
629
|
-
if (!cookieHeader) return undefined
|
|
630
|
-
const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${name}=([^;]+)`))
|
|
631
|
-
return match ? match[1] : undefined
|
|
632
|
-
}
|
|
633
|
-
|
|
634
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
|