@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
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, mock } from 'bun:test'
|
|
2
|
+
import { randomUUID, createHash } from 'node:crypto'
|
|
3
|
+
import { mockConfigModule } from './helpers/mock-config'
|
|
4
|
+
|
|
5
|
+
// Mock config before imports
|
|
6
|
+
mock.module('../config', () => mockConfigModule())
|
|
7
|
+
|
|
8
|
+
import { Hono } from 'hono'
|
|
9
|
+
|
|
10
|
+
let authRoutes: Hono | undefined
|
|
11
|
+
let webAuth: Hono | undefined
|
|
12
|
+
let resetDbSingleton: (() => void) | undefined
|
|
13
|
+
let getDb: (() => import('bun:sqlite').Database) | undefined
|
|
14
|
+
let resetRateLimits: (() => void) | undefined
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const dbMod = await import('../db/sqlite')
|
|
18
|
+
getDb = dbMod.getDb
|
|
19
|
+
resetDbSingleton = dbMod.resetDbSingleton
|
|
20
|
+
} catch {
|
|
21
|
+
// Module may not exist yet
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const mod = await import('../routes/web/auth-routes')
|
|
26
|
+
authRoutes = mod.default
|
|
27
|
+
resetRateLimits = mod._resetRateLimits
|
|
28
|
+
} catch {
|
|
29
|
+
// Module may not exist yet
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const mod = await import('../routes/web/auth')
|
|
34
|
+
webAuth = mod.default
|
|
35
|
+
} catch {
|
|
36
|
+
// Module may not exist yet
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function freshDb() {
|
|
40
|
+
if (resetDbSingleton) {
|
|
41
|
+
try {
|
|
42
|
+
resetDbSingleton()
|
|
43
|
+
} catch {
|
|
44
|
+
// ignore
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (getDb) {
|
|
48
|
+
const db = getDb()
|
|
49
|
+
try {
|
|
50
|
+
db.exec('DELETE FROM session_tokens')
|
|
51
|
+
db.exec('DELETE FROM users')
|
|
52
|
+
} catch {
|
|
53
|
+
// tables may not exist yet
|
|
54
|
+
}
|
|
55
|
+
return db
|
|
56
|
+
}
|
|
57
|
+
throw new Error('getDb not available')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function createApp(): Hono {
|
|
61
|
+
const app = new Hono()
|
|
62
|
+
if (webAuth) app.route('/web', webAuth)
|
|
63
|
+
if (authRoutes) app.route('/web/auth', authRoutes)
|
|
64
|
+
return app
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ===========================================================================
|
|
68
|
+
// Task 1: /web/bind returns 410 Gone
|
|
69
|
+
// ===========================================================================
|
|
70
|
+
describe('Security: /web/bind removed', () => {
|
|
71
|
+
let app: Hono
|
|
72
|
+
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
freshDb()
|
|
75
|
+
app = createApp()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('POST /web/bind returns 410 Gone', async () => {
|
|
79
|
+
const res = await app.request('/web/bind', {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers: { 'Content-Type': 'application/json' },
|
|
82
|
+
body: JSON.stringify({ sessionId: 'ses_123', uuid: 'some-uuid' }),
|
|
83
|
+
})
|
|
84
|
+
expect(res.status).toBe(410)
|
|
85
|
+
const body = await res.json()
|
|
86
|
+
expect(body.error).toBeDefined()
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// ===========================================================================
|
|
91
|
+
// Task 3: /login timing side-channel mitigation
|
|
92
|
+
// ===========================================================================
|
|
93
|
+
describe('Security: /login timing side-channel', () => {
|
|
94
|
+
let app: Hono
|
|
95
|
+
|
|
96
|
+
beforeEach(async () => {
|
|
97
|
+
freshDb()
|
|
98
|
+
if (resetRateLimits) resetRateLimits()
|
|
99
|
+
app = createApp()
|
|
100
|
+
// Create admin via setup
|
|
101
|
+
await app.request('/web/auth/setup', {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers: { 'Content-Type': 'application/json' },
|
|
104
|
+
body: JSON.stringify({
|
|
105
|
+
apiKey: 'test-api-key',
|
|
106
|
+
username: 'admin',
|
|
107
|
+
password: 'admin-password-123',
|
|
108
|
+
}),
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('both non-existent user and wrong password return same error message', async () => {
|
|
113
|
+
const resNonExistent = await app.request('/web/auth/login', {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: { 'Content-Type': 'application/json' },
|
|
116
|
+
body: JSON.stringify({
|
|
117
|
+
username: 'nonexistent_user',
|
|
118
|
+
password: 'some-password-123',
|
|
119
|
+
}),
|
|
120
|
+
})
|
|
121
|
+
const bodyNonExistent = await resNonExistent.json()
|
|
122
|
+
|
|
123
|
+
const resWrongPass = await app.request('/web/auth/login', {
|
|
124
|
+
method: 'POST',
|
|
125
|
+
headers: { 'Content-Type': 'application/json' },
|
|
126
|
+
body: JSON.stringify({
|
|
127
|
+
username: 'admin',
|
|
128
|
+
password: 'wrong-password-123',
|
|
129
|
+
}),
|
|
130
|
+
})
|
|
131
|
+
const bodyWrongPass = await resWrongPass.json()
|
|
132
|
+
|
|
133
|
+
// Both return 401
|
|
134
|
+
expect(resNonExistent.status).toBe(401)
|
|
135
|
+
expect(resWrongPass.status).toBe(401)
|
|
136
|
+
|
|
137
|
+
// Both return the same generic error message (does not leak user existence)
|
|
138
|
+
expect(bodyNonExistent.error).toBe(bodyWrongPass.error)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
test(
|
|
142
|
+
'non-existent user login takes similar time to wrong-password login',
|
|
143
|
+
async () => {
|
|
144
|
+
// Measure timing for non-existent user (should run dummy hash)
|
|
145
|
+
const startNonExistent = performance.now()
|
|
146
|
+
for (let i = 0; i < 3; i++) {
|
|
147
|
+
await app.request('/web/auth/login', {
|
|
148
|
+
method: 'POST',
|
|
149
|
+
headers: {
|
|
150
|
+
'Content-Type': 'application/json',
|
|
151
|
+
'X-Forwarded-For': `10.0.0.${10 + i}`,
|
|
152
|
+
},
|
|
153
|
+
body: JSON.stringify({
|
|
154
|
+
username: 'nonexistent_user_xyz',
|
|
155
|
+
password: 'some-password-123',
|
|
156
|
+
}),
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
const elapsedNonExistent = performance.now() - startNonExistent
|
|
160
|
+
|
|
161
|
+
// Measure timing for wrong password
|
|
162
|
+
const startWrongPass = performance.now()
|
|
163
|
+
for (let i = 0; i < 3; i++) {
|
|
164
|
+
await app.request('/web/auth/login', {
|
|
165
|
+
method: 'POST',
|
|
166
|
+
headers: {
|
|
167
|
+
'Content-Type': 'application/json',
|
|
168
|
+
'X-Forwarded-For': `10.0.1.${10 + i}`,
|
|
169
|
+
},
|
|
170
|
+
body: JSON.stringify({
|
|
171
|
+
username: 'admin',
|
|
172
|
+
password: 'wrong-password-xyz',
|
|
173
|
+
}),
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
const elapsedWrongPass = performance.now() - startWrongPass
|
|
177
|
+
|
|
178
|
+
// Non-existent user should take at least 50% of the time of wrong-password
|
|
179
|
+
// (both should invoke argon2id verification)
|
|
180
|
+
// This is a loose bound to avoid flaky tests — the point is that
|
|
181
|
+
// non-existent user is NOT an order of magnitude faster.
|
|
182
|
+
const ratio = elapsedNonExistent / elapsedWrongPass
|
|
183
|
+
expect(ratio).toBeGreaterThan(0.3)
|
|
184
|
+
},
|
|
185
|
+
{ timeout: 60000 },
|
|
186
|
+
)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
// ===========================================================================
|
|
190
|
+
// Task 2: consumed_at semantics — max_uses > 1 invitations
|
|
191
|
+
// ===========================================================================
|
|
192
|
+
describe('Security: consumed_at semantics for multi-use invitations', () => {
|
|
193
|
+
let app: Hono
|
|
194
|
+
let inviteToken: string
|
|
195
|
+
|
|
196
|
+
beforeEach(async () => {
|
|
197
|
+
freshDb()
|
|
198
|
+
if (resetRateLimits) resetRateLimits()
|
|
199
|
+
app = createApp()
|
|
200
|
+
// Setup admin
|
|
201
|
+
await app.request('/web/auth/setup', {
|
|
202
|
+
method: 'POST',
|
|
203
|
+
headers: { 'Content-Type': 'application/json' },
|
|
204
|
+
body: JSON.stringify({
|
|
205
|
+
apiKey: 'test-api-key',
|
|
206
|
+
username: 'admin',
|
|
207
|
+
password: 'admin-password-123',
|
|
208
|
+
}),
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
// Create a max_uses=3 invitation
|
|
212
|
+
const db = getDb!()
|
|
213
|
+
inviteToken = `inv_multi_${randomUUID().slice(0, 8)}`
|
|
214
|
+
const tokenHash = createHash('sha256').update(inviteToken).digest('hex')
|
|
215
|
+
const futureExpiry = new Date(Date.now() + 86400000).toISOString()
|
|
216
|
+
const adminRow = db
|
|
217
|
+
.query("SELECT id FROM users WHERE username = 'admin'")
|
|
218
|
+
.get() as { id: string }
|
|
219
|
+
db.exec(
|
|
220
|
+
`INSERT INTO invitations (token_hash, role, expires_at, max_uses, uses, created_by)
|
|
221
|
+
VALUES ('${tokenHash}', 'member', '${futureExpiry}', 3, 0, '${adminRow.id}')`,
|
|
222
|
+
)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
test(
|
|
226
|
+
'multi-use invitation (max_uses=3) allows multiple joins',
|
|
227
|
+
async () => {
|
|
228
|
+
// First join should succeed
|
|
229
|
+
const res1 = await app.request('/web/auth/join', {
|
|
230
|
+
method: 'POST',
|
|
231
|
+
headers: { 'Content-Type': 'application/json' },
|
|
232
|
+
body: JSON.stringify({
|
|
233
|
+
inviteToken,
|
|
234
|
+
username: 'user_one',
|
|
235
|
+
password: 'user-one-password-123',
|
|
236
|
+
}),
|
|
237
|
+
})
|
|
238
|
+
expect(res1.status).toBe(200)
|
|
239
|
+
|
|
240
|
+
// Second join with same invitation should also succeed
|
|
241
|
+
const res2 = await app.request('/web/auth/join', {
|
|
242
|
+
method: 'POST',
|
|
243
|
+
headers: { 'Content-Type': 'application/json' },
|
|
244
|
+
body: JSON.stringify({
|
|
245
|
+
inviteToken,
|
|
246
|
+
username: 'user_two',
|
|
247
|
+
password: 'user-two-password-123',
|
|
248
|
+
}),
|
|
249
|
+
})
|
|
250
|
+
expect(res2.status).toBe(200)
|
|
251
|
+
|
|
252
|
+
// Third join should succeed (uses 3 of 3)
|
|
253
|
+
const res3 = await app.request('/web/auth/join', {
|
|
254
|
+
method: 'POST',
|
|
255
|
+
headers: { 'Content-Type': 'application/json' },
|
|
256
|
+
body: JSON.stringify({
|
|
257
|
+
inviteToken,
|
|
258
|
+
username: 'user_three',
|
|
259
|
+
password: 'user-three-password-123',
|
|
260
|
+
}),
|
|
261
|
+
})
|
|
262
|
+
expect(res3.status).toBe(200)
|
|
263
|
+
|
|
264
|
+
// Fourth join should fail (max_uses reached)
|
|
265
|
+
const res4 = await app.request('/web/auth/join', {
|
|
266
|
+
method: 'POST',
|
|
267
|
+
headers: { 'Content-Type': 'application/json' },
|
|
268
|
+
body: JSON.stringify({
|
|
269
|
+
inviteToken,
|
|
270
|
+
username: 'user_four',
|
|
271
|
+
password: 'user-four-password-123',
|
|
272
|
+
}),
|
|
273
|
+
})
|
|
274
|
+
expect(res4.status).toBe(400)
|
|
275
|
+
|
|
276
|
+
// Verify 4 users exist: admin + 3 joined
|
|
277
|
+
const db = getDb!()
|
|
278
|
+
const count = db
|
|
279
|
+
.query('SELECT COUNT(*) as count FROM users')
|
|
280
|
+
.get() as { count: number }
|
|
281
|
+
expect(count.count).toBe(4)
|
|
282
|
+
|
|
283
|
+
// Verify invitation uses count
|
|
284
|
+
const tokenHash = createHash('sha256')
|
|
285
|
+
.update(inviteToken)
|
|
286
|
+
.digest('hex')
|
|
287
|
+
const inv = db
|
|
288
|
+
.query('SELECT uses, max_uses, consumed_at FROM invitations WHERE token_hash = $hash')
|
|
289
|
+
.get({ $hash: tokenHash }) as {
|
|
290
|
+
uses: number
|
|
291
|
+
max_uses: number
|
|
292
|
+
consumed_at: string | null
|
|
293
|
+
}
|
|
294
|
+
expect(inv.uses).toBe(3)
|
|
295
|
+
expect(inv.max_uses).toBe(3)
|
|
296
|
+
// consumed_at should be set after max_uses reached
|
|
297
|
+
expect(inv.consumed_at).not.toBeNull()
|
|
298
|
+
},
|
|
299
|
+
{ timeout: 60000 },
|
|
300
|
+
)
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
// ===========================================================================
|
|
304
|
+
// Task 4: sessionAuth isAdmin from DB role
|
|
305
|
+
// ===========================================================================
|
|
306
|
+
describe('Security: sessionAuth isAdmin from DB role', () => {
|
|
307
|
+
let app: Hono
|
|
308
|
+
let adminAccessToken: string
|
|
309
|
+
|
|
310
|
+
beforeEach(async () => {
|
|
311
|
+
freshDb()
|
|
312
|
+
if (resetRateLimits) resetRateLimits()
|
|
313
|
+
app = createApp()
|
|
314
|
+
// Setup admin
|
|
315
|
+
await app.request('/web/auth/setup', {
|
|
316
|
+
method: 'POST',
|
|
317
|
+
headers: { 'Content-Type': 'application/json' },
|
|
318
|
+
body: JSON.stringify({
|
|
319
|
+
apiKey: 'test-api-key',
|
|
320
|
+
username: 'admin',
|
|
321
|
+
password: 'admin-password-123',
|
|
322
|
+
}),
|
|
323
|
+
})
|
|
324
|
+
// Login as admin
|
|
325
|
+
const loginRes = await app.request('/web/auth/login', {
|
|
326
|
+
method: 'POST',
|
|
327
|
+
headers: { 'Content-Type': 'application/json' },
|
|
328
|
+
body: JSON.stringify({
|
|
329
|
+
username: 'admin',
|
|
330
|
+
password: 'admin-password-123',
|
|
331
|
+
}),
|
|
332
|
+
})
|
|
333
|
+
const loginBody = await loginRes.json()
|
|
334
|
+
adminAccessToken = loginBody.accessToken || loginBody.access_token || ''
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
test('cookie-based admin login sets isAdmin=true', async () => {
|
|
338
|
+
// Create a protected route that exposes isAdmin
|
|
339
|
+
const { sessionAuth } = await import('../auth/middleware')
|
|
340
|
+
const testApp = new Hono()
|
|
341
|
+
testApp.get('/protected', async (c, next) => {
|
|
342
|
+
const result = await sessionAuth(c, next)
|
|
343
|
+
if (result) return result
|
|
344
|
+
return c.json({
|
|
345
|
+
userId: c.get('userId'),
|
|
346
|
+
isAdmin: c.get('isAdmin'),
|
|
347
|
+
})
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
// Use Bearer token auth (simulates cookie-based resolution via DB)
|
|
351
|
+
const res = await testApp.request('/protected', {
|
|
352
|
+
headers: { Authorization: `Bearer ${adminAccessToken}` },
|
|
353
|
+
})
|
|
354
|
+
expect(res.status).toBe(200)
|
|
355
|
+
const body = await res.json()
|
|
356
|
+
expect(body.isAdmin).toBe(true)
|
|
357
|
+
})
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
// ===========================================================================
|
|
361
|
+
// Task 5: /join username UNIQUE race returns 409 not 500
|
|
362
|
+
// ===========================================================================
|
|
363
|
+
describe('Security: /join username race returns 409', () => {
|
|
364
|
+
let app: Hono
|
|
365
|
+
let inviteToken1: string
|
|
366
|
+
let inviteToken2: string
|
|
367
|
+
|
|
368
|
+
beforeEach(async () => {
|
|
369
|
+
freshDb()
|
|
370
|
+
if (resetRateLimits) resetRateLimits()
|
|
371
|
+
app = createApp()
|
|
372
|
+
// Setup admin
|
|
373
|
+
await app.request('/web/auth/setup', {
|
|
374
|
+
method: 'POST',
|
|
375
|
+
headers: { 'Content-Type': 'application/json' },
|
|
376
|
+
body: JSON.stringify({
|
|
377
|
+
apiKey: 'test-api-key',
|
|
378
|
+
username: 'admin',
|
|
379
|
+
password: 'admin-password-123',
|
|
380
|
+
}),
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
// Create two separate max_uses=1 invitations so two joins can proceed
|
|
384
|
+
// simultaneously — the only conflict will be username uniqueness
|
|
385
|
+
const db = getDb!()
|
|
386
|
+
const futureExpiry = new Date(Date.now() + 86400000).toISOString()
|
|
387
|
+
const adminRow = db
|
|
388
|
+
.query("SELECT id FROM users WHERE username = 'admin'")
|
|
389
|
+
.get() as { id: string }
|
|
390
|
+
|
|
391
|
+
inviteToken1 = `inv_race1_${randomUUID().slice(0, 8)}`
|
|
392
|
+
const hash1 = createHash('sha256').update(inviteToken1).digest('hex')
|
|
393
|
+
db.exec(
|
|
394
|
+
`INSERT INTO invitations (token_hash, role, expires_at, max_uses, uses, created_by)
|
|
395
|
+
VALUES ('${hash1}', 'member', '${futureExpiry}', 1, 0, '${adminRow.id}')`,
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
inviteToken2 = `inv_race2_${randomUUID().slice(0, 8)}`
|
|
399
|
+
const hash2 = createHash('sha256').update(inviteToken2).digest('hex')
|
|
400
|
+
db.exec(
|
|
401
|
+
`INSERT INTO invitations (token_hash, role, expires_at, max_uses, uses, created_by)
|
|
402
|
+
VALUES ('${hash2}', 'member', '${futureExpiry}', 1, 0, '${adminRow.id}')`,
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
// Pre-insert a user with the conflicting username
|
|
406
|
+
db.exec(
|
|
407
|
+
`INSERT INTO users (id, username, password_hash, role, created_at)
|
|
408
|
+
VALUES ('usr_existing', 'duplicate_user', 'hash', 'member', '2026-01-01T00:00:00Z')`,
|
|
409
|
+
)
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
test('join with already-taken username returns 409 not 500', async () => {
|
|
413
|
+
const res = await app.request('/web/auth/join', {
|
|
414
|
+
method: 'POST',
|
|
415
|
+
headers: { 'Content-Type': 'application/json' },
|
|
416
|
+
body: JSON.stringify({
|
|
417
|
+
inviteToken: inviteToken1,
|
|
418
|
+
username: 'duplicate_user',
|
|
419
|
+
password: 'some-password-123',
|
|
420
|
+
}),
|
|
421
|
+
})
|
|
422
|
+
// Should return 409 Conflict, not 500
|
|
423
|
+
expect(res.status).toBe(409)
|
|
424
|
+
const body = await res.json()
|
|
425
|
+
expect(body.error).toMatch(/[Uu]sername/)
|
|
426
|
+
})
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
// ===========================================================================
|
|
430
|
+
// Task 8: CSRF middleware
|
|
431
|
+
// ===========================================================================
|
|
432
|
+
describe('Security: CSRF middleware', () => {
|
|
433
|
+
test('allows GET requests without Origin', async () => {
|
|
434
|
+
const { csrfCheck } = await import('../auth/csrf')
|
|
435
|
+
const app = new Hono()
|
|
436
|
+
app.use('/web/*', csrfCheck)
|
|
437
|
+
app.get('/web/ping', c => c.text('ok'))
|
|
438
|
+
|
|
439
|
+
const res = await app.request('/web/ping')
|
|
440
|
+
expect(res.status).toBe(200)
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
test('allows POST with matching Origin', async () => {
|
|
444
|
+
const { csrfCheck } = await import('../auth/csrf')
|
|
445
|
+
const app = new Hono()
|
|
446
|
+
app.use('/web/*', csrfCheck)
|
|
447
|
+
app.post('/web/action', c => c.json({ ok: true }))
|
|
448
|
+
|
|
449
|
+
const res = await app.request('http://localhost:3000/web/action', {
|
|
450
|
+
method: 'POST',
|
|
451
|
+
headers: {
|
|
452
|
+
Origin: 'http://localhost:3000',
|
|
453
|
+
'Content-Type': 'application/json',
|
|
454
|
+
},
|
|
455
|
+
})
|
|
456
|
+
expect(res.status).toBe(200)
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
test('rejects POST with mismatched Origin', async () => {
|
|
460
|
+
const { csrfCheck } = await import('../auth/csrf')
|
|
461
|
+
const app = new Hono()
|
|
462
|
+
app.use('/web/*', csrfCheck)
|
|
463
|
+
app.post('/web/action', c => c.json({ ok: true }))
|
|
464
|
+
|
|
465
|
+
const res = await app.request('http://localhost:3000/web/action', {
|
|
466
|
+
method: 'POST',
|
|
467
|
+
headers: {
|
|
468
|
+
Origin: 'https://attacker.example',
|
|
469
|
+
'Content-Type': 'application/json',
|
|
470
|
+
},
|
|
471
|
+
})
|
|
472
|
+
expect(res.status).toBe(403)
|
|
473
|
+
const body = await res.json()
|
|
474
|
+
expect(body.error).toContain('CSRF')
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
test('allows POST without Origin (non-browser client)', async () => {
|
|
478
|
+
const { csrfCheck } = await import('../auth/csrf')
|
|
479
|
+
const app = new Hono()
|
|
480
|
+
app.use('/web/*', csrfCheck)
|
|
481
|
+
app.post('/web/action', c => c.json({ ok: true }))
|
|
482
|
+
|
|
483
|
+
const res = await app.request('http://localhost:3000/web/action', {
|
|
484
|
+
method: 'POST',
|
|
485
|
+
headers: { 'Content-Type': 'application/json' },
|
|
486
|
+
})
|
|
487
|
+
expect(res.status).toBe(200)
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
test('allows DELETE with matching Origin', async () => {
|
|
491
|
+
const { csrfCheck } = await import('../auth/csrf')
|
|
492
|
+
const app = new Hono()
|
|
493
|
+
app.use('/web/*', csrfCheck)
|
|
494
|
+
app.delete('/web/resource/:id', c => c.json({ ok: true }))
|
|
495
|
+
|
|
496
|
+
const res = await app.request(
|
|
497
|
+
'http://localhost:3000/web/resource/abc',
|
|
498
|
+
{
|
|
499
|
+
method: 'DELETE',
|
|
500
|
+
headers: {
|
|
501
|
+
Origin: 'http://localhost:3000',
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
)
|
|
505
|
+
expect(res.status).toBe(200)
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
test('rejects PUT with mismatched Origin', async () => {
|
|
509
|
+
const { csrfCheck } = await import('../auth/csrf')
|
|
510
|
+
const app = new Hono()
|
|
511
|
+
app.use('/web/*', csrfCheck)
|
|
512
|
+
app.put('/web/resource/:id', c => c.json({ ok: true }))
|
|
513
|
+
|
|
514
|
+
const res = await app.request(
|
|
515
|
+
'http://localhost:3000/web/resource/abc',
|
|
516
|
+
{
|
|
517
|
+
method: 'PUT',
|
|
518
|
+
headers: {
|
|
519
|
+
Origin: 'https://evil.example',
|
|
520
|
+
'Content-Type': 'application/json',
|
|
521
|
+
},
|
|
522
|
+
},
|
|
523
|
+
)
|
|
524
|
+
expect(res.status).toBe(403)
|
|
525
|
+
})
|
|
526
|
+
})
|