@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.
Files changed (167) hide show
  1. package/dist/cli.js +212 -140
  2. package/dist/rcs-web/assets/Dashboard-YYwDrKTN.js +1 -0
  3. package/dist/rcs-web/assets/Join-DKCP5X6X.js +1 -0
  4. package/dist/rcs-web/assets/Login-0VGZs85I.js +1 -0
  5. package/{packages/remote-control-server/web/dist/assets/SessionDetail-DSoMXZJb.js → dist/rcs-web/assets/SessionDetail-DmK-DfBQ.js} +11 -11
  6. package/dist/rcs-web/assets/Setup-C0cjxXMt.js +1 -0
  7. package/dist/rcs-web/assets/{ShareLanding-C06U9Nrr.js → ShareLanding-DUZrreJ7.js} +1 -1
  8. package/dist/rcs-web/assets/TeamCreate-B8ZV6EdY.js +1 -0
  9. package/dist/rcs-web/assets/{TeamDetail-d_GwIRHd.js → TeamDetail-DBAsJ__i.js} +2 -2
  10. package/dist/rcs-web/assets/{TeamList-BlNGdKci.js → TeamList-C-Hte4nV.js} +1 -1
  11. package/dist/rcs-web/assets/{TeamSettings-CPraC9uS.js → TeamSettings-CNVwYwcp.js} +1 -1
  12. package/dist/rcs-web/assets/UserSettings-DRdz9kYE.js +1 -0
  13. package/dist/rcs-web/assets/{arc-BmYza-0n.js → arc-CJxirXEQ.js} +1 -1
  14. package/dist/rcs-web/assets/{architectureDiagram-3BPJPVTR-CYBtlgdO.js → architectureDiagram-3BPJPVTR-BZvIHrLA.js} +1 -1
  15. package/dist/rcs-web/assets/{blockDiagram-GPEHLZMM-RJAfHk_a.js → blockDiagram-GPEHLZMM-3p0y3Ija.js} +1 -1
  16. package/{packages/remote-control-server/web/dist/assets/c4Diagram-AAUBKEIU-CJ-qJA6w.js → dist/rcs-web/assets/c4Diagram-AAUBKEIU-Bmz9A1wn.js} +1 -1
  17. package/dist/rcs-web/assets/channel-DaI_n8dp.js +1 -0
  18. package/dist/rcs-web/assets/{chunk-2J33WTMH-n5I6Q1zJ.js → chunk-2J33WTMH-B8gQz22j.js} +1 -1
  19. package/dist/rcs-web/assets/{chunk-4BX2VUAB-CSOBh7qY.js → chunk-4BX2VUAB-CgLEZsTU.js} +1 -1
  20. package/dist/rcs-web/assets/{chunk-55IACEB6-C0euQVm_.js → chunk-55IACEB6-0mdE_-IS.js} +1 -1
  21. package/{packages/remote-control-server/web/dist/assets/chunk-727SXJPM-Ch7yD8cR.js → dist/rcs-web/assets/chunk-727SXJPM-DdtjnIHR.js} +1 -1
  22. package/{packages/remote-control-server/web/dist/assets/chunk-AQP2D5EJ-Ms2Q7r6u.js → dist/rcs-web/assets/chunk-AQP2D5EJ-BtwFPk50.js} +1 -1
  23. package/dist/rcs-web/assets/{chunk-FMBD7UC4-bszHUkpx.js → chunk-FMBD7UC4-BcO9mQ0P.js} +1 -1
  24. package/dist/rcs-web/assets/{chunk-ND2GUHAM-BVQIhq6J.js → chunk-ND2GUHAM-BvRq13S_.js} +1 -1
  25. package/dist/rcs-web/assets/{chunk-QZHKN3VN-BVEDe5qc.js → chunk-QZHKN3VN-D7GHkPpq.js} +1 -1
  26. package/dist/rcs-web/assets/classDiagram-4FO5ZUOK-pYllpkOq.js +1 -0
  27. package/dist/rcs-web/assets/classDiagram-v2-Q7XG4LA2-pYllpkOq.js +1 -0
  28. package/dist/rcs-web/assets/{code-block-IT6T5CEO-B26n-rFC.js → code-block-IT6T5CEO-B1p-vO-3.js} +1 -1
  29. package/{packages/remote-control-server/web/dist/assets/cose-bilkent-S5V4N54A-DXc7GE8W.js → dist/rcs-web/assets/cose-bilkent-S5V4N54A-L0rUZuH5.js} +1 -1
  30. package/dist/rcs-web/assets/{dagre-BM42HDAG-DAXofHt_.js → dagre-BM42HDAG-CHGuio22.js} +1 -1
  31. package/dist/rcs-web/assets/{diagram-2AECGRRQ-DK0TBUQA.js → diagram-2AECGRRQ-DIDRyQk8.js} +1 -1
  32. package/dist/rcs-web/assets/{diagram-5GNKFQAL-DMsq-Grp.js → diagram-5GNKFQAL-CNubKb6P.js} +1 -1
  33. package/dist/rcs-web/assets/{diagram-KO2AKTUF-B3FMhmUc.js → diagram-KO2AKTUF-CzQh664-.js} +1 -1
  34. package/dist/rcs-web/assets/{diagram-LMA3HP47-BjO3BHFl.js → diagram-LMA3HP47-CWBR7-VG.js} +1 -1
  35. package/dist/rcs-web/assets/{diagram-OG6HWLK6-CNg4B2L3.js → diagram-OG6HWLK6-BMHdlbcZ.js} +1 -1
  36. package/{packages/remote-control-server/web/dist/assets/erDiagram-TEJ5UH35-BDxz8NRK.js → dist/rcs-web/assets/erDiagram-TEJ5UH35-OgT8dC-K.js} +1 -1
  37. package/{packages/remote-control-server/web/dist/assets/flowDiagram-I6XJVG4X-VIgM13sK.js → dist/rcs-web/assets/flowDiagram-I6XJVG4X-DRrCY2oB.js} +1 -1
  38. package/dist/rcs-web/assets/{ganttDiagram-6RSMTGT7-Ba2QuJU0.js → ganttDiagram-6RSMTGT7-DctpzEKN.js} +1 -1
  39. package/dist/rcs-web/assets/{gitGraphDiagram-PVQCEYII-B-gV-lAg.js → gitGraphDiagram-PVQCEYII-5KpLszm9.js} +1 -1
  40. package/dist/rcs-web/assets/{infoDiagram-5YYISTIA-DothwgD4.js → infoDiagram-5YYISTIA-BAKD_Tt7.js} +1 -1
  41. package/{packages/remote-control-server/web/dist/assets/ishikawaDiagram-YF4QCWOH-DhpjM1Vc.js → dist/rcs-web/assets/ishikawaDiagram-YF4QCWOH-VHOPG4Nc.js} +1 -1
  42. package/dist/rcs-web/assets/{journeyDiagram-JHISSGLW-PbQTouMJ.js → journeyDiagram-JHISSGLW-BtGih-ik.js} +1 -1
  43. package/dist/rcs-web/assets/{kanban-definition-UN3LZRKU-63WdwG-X.js → kanban-definition-UN3LZRKU-BsEGZsD1.js} +1 -1
  44. package/dist/rcs-web/assets/{linear-CeRljV9s.js → linear-Dj636PNG.js} +1 -1
  45. package/dist/rcs-web/assets/main-CkqtCllF.js +238 -0
  46. package/dist/rcs-web/assets/{mermaid-VLURNSYL-CG6yNR7g.js → mermaid-VLURNSYL-DutpHHm9.js} +4 -4
  47. package/dist/rcs-web/assets/{mermaid.core-CAuNNATY.js → mermaid.core-XHHf0pen.js} +5 -5
  48. package/dist/rcs-web/assets/{mindmap-definition-RKZ34NQL-9hDL1CUV.js → mindmap-definition-RKZ34NQL-BBa8V_3g.js} +1 -1
  49. package/dist/rcs-web/assets/{pieDiagram-4H26LBE5-BS_P2t04.js → pieDiagram-4H26LBE5-BuNEdo9H.js} +1 -1
  50. package/dist/rcs-web/assets/{quadrantDiagram-W4KKPZXB-Cp9QQkHV.js → quadrantDiagram-W4KKPZXB-CsJgyQIN.js} +1 -1
  51. package/dist/rcs-web/assets/{requirementDiagram-4Y6WPE33-C_XSzOKE.js → requirementDiagram-4Y6WPE33-DqMJZ914.js} +1 -1
  52. package/{packages/remote-control-server/web/dist/assets/sankeyDiagram-5OEKKPKP-BhuVqzZd.js → dist/rcs-web/assets/sankeyDiagram-5OEKKPKP-CVsz09q2.js} +1 -1
  53. package/{packages/remote-control-server/web/dist/assets/sequenceDiagram-3UESZ5HK-C3IPZrcW.js → dist/rcs-web/assets/sequenceDiagram-3UESZ5HK-BRp4Wddn.js} +1 -1
  54. package/dist/rcs-web/assets/{stateDiagram-AJRCARHV-CN4iNPRl.js → stateDiagram-AJRCARHV-S-QqQr-D.js} +1 -1
  55. package/dist/rcs-web/assets/{stateDiagram-v2-BHNVJYJU-CICBlDSJ.js → stateDiagram-v2-BHNVJYJU-D77KfDr-.js} +1 -1
  56. 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
  57. package/dist/rcs-web/assets/{useTeams-DPHGnBHL.js → useTeams-CqJOqEy1.js} +1 -1
  58. package/dist/rcs-web/assets/{vennDiagram-CIIHVFJN-CSaUc2Xv.js → vennDiagram-CIIHVFJN-Dikwrn1L.js} +1 -1
  59. package/dist/rcs-web/assets/{wardleyDiagram-YWT4CUSO-DTuJrl9P.js → wardleyDiagram-YWT4CUSO-DUzpiV6H.js} +1 -1
  60. package/dist/rcs-web/assets/{workerType-CSqeySYe.js → workerType-DOQ0AHrq.js} +1 -1
  61. package/dist/rcs-web/assets/{xychartDiagram-2RQKCTM6-BQ0oU3Qi.js → xychartDiagram-2RQKCTM6-OZNbB32D.js} +1 -1
  62. package/dist/rcs-web/index.html +1 -1
  63. package/package.json +2 -2
  64. package/packages/remote-control-server/src/__tests__/cookie.test.ts +0 -71
  65. package/packages/remote-control-server/src/__tests__/data-layer-round2.test.ts +556 -0
  66. package/packages/remote-control-server/src/__tests__/route-mounting.test.ts +212 -0
  67. package/packages/remote-control-server/src/__tests__/routes.test.ts +5 -44
  68. package/packages/remote-control-server/src/__tests__/security-fixes.test.ts +526 -0
  69. package/packages/remote-control-server/src/__tests__/security-round3.test.ts +780 -0
  70. package/packages/remote-control-server/src/__tests__/shares.test.ts +5 -2
  71. package/packages/remote-control-server/src/auth/constants.ts +0 -6
  72. package/packages/remote-control-server/src/auth/cookie.ts +0 -43
  73. package/packages/remote-control-server/src/auth/csrf.ts +58 -0
  74. package/packages/remote-control-server/src/auth/middleware.ts +7 -4
  75. package/packages/remote-control-server/src/index.ts +26 -0
  76. package/packages/remote-control-server/src/registerRoutes.ts +17 -1
  77. package/packages/remote-control-server/src/routes/web/auth-routes.ts +93 -54
  78. package/packages/remote-control-server/src/routes/web/auth.ts +16 -23
  79. package/packages/remote-control-server/src/routes/web/sessions.ts +30 -5
  80. package/packages/remote-control-server/src/routes/web/shares.ts +38 -1
  81. package/packages/remote-control-server/src/routes/web/teams.ts +22 -4
  82. package/packages/remote-control-server/src/services/session.ts +143 -9
  83. package/packages/remote-control-server/src/store.ts +34 -1
  84. package/packages/remote-control-server/src/types/api.ts +0 -1
  85. package/packages/remote-control-server/web/dist/assets/Dashboard-YYwDrKTN.js +1 -0
  86. package/packages/remote-control-server/web/dist/assets/Join-DKCP5X6X.js +1 -0
  87. package/packages/remote-control-server/web/dist/assets/Login-0VGZs85I.js +1 -0
  88. package/{dist/rcs-web/assets/SessionDetail-DSoMXZJb.js → packages/remote-control-server/web/dist/assets/SessionDetail-DmK-DfBQ.js} +11 -11
  89. package/packages/remote-control-server/web/dist/assets/Setup-C0cjxXMt.js +1 -0
  90. package/packages/remote-control-server/web/dist/assets/{ShareLanding-C06U9Nrr.js → ShareLanding-DUZrreJ7.js} +1 -1
  91. package/packages/remote-control-server/web/dist/assets/TeamCreate-B8ZV6EdY.js +1 -0
  92. package/packages/remote-control-server/web/dist/assets/{TeamDetail-d_GwIRHd.js → TeamDetail-DBAsJ__i.js} +2 -2
  93. package/packages/remote-control-server/web/dist/assets/{TeamList-BlNGdKci.js → TeamList-C-Hte4nV.js} +1 -1
  94. package/packages/remote-control-server/web/dist/assets/{TeamSettings-CPraC9uS.js → TeamSettings-CNVwYwcp.js} +1 -1
  95. package/packages/remote-control-server/web/dist/assets/UserSettings-DRdz9kYE.js +1 -0
  96. package/packages/remote-control-server/web/dist/assets/{arc-BmYza-0n.js → arc-CJxirXEQ.js} +1 -1
  97. package/packages/remote-control-server/web/dist/assets/{architectureDiagram-3BPJPVTR-CYBtlgdO.js → architectureDiagram-3BPJPVTR-BZvIHrLA.js} +1 -1
  98. package/packages/remote-control-server/web/dist/assets/{blockDiagram-GPEHLZMM-RJAfHk_a.js → blockDiagram-GPEHLZMM-3p0y3Ija.js} +1 -1
  99. package/{dist/rcs-web/assets/c4Diagram-AAUBKEIU-CJ-qJA6w.js → packages/remote-control-server/web/dist/assets/c4Diagram-AAUBKEIU-Bmz9A1wn.js} +1 -1
  100. package/packages/remote-control-server/web/dist/assets/channel-DaI_n8dp.js +1 -0
  101. package/packages/remote-control-server/web/dist/assets/{chunk-2J33WTMH-n5I6Q1zJ.js → chunk-2J33WTMH-B8gQz22j.js} +1 -1
  102. package/packages/remote-control-server/web/dist/assets/{chunk-4BX2VUAB-CSOBh7qY.js → chunk-4BX2VUAB-CgLEZsTU.js} +1 -1
  103. package/packages/remote-control-server/web/dist/assets/{chunk-55IACEB6-C0euQVm_.js → chunk-55IACEB6-0mdE_-IS.js} +1 -1
  104. package/{dist/rcs-web/assets/chunk-727SXJPM-Ch7yD8cR.js → packages/remote-control-server/web/dist/assets/chunk-727SXJPM-DdtjnIHR.js} +1 -1
  105. package/{dist/rcs-web/assets/chunk-AQP2D5EJ-Ms2Q7r6u.js → packages/remote-control-server/web/dist/assets/chunk-AQP2D5EJ-BtwFPk50.js} +1 -1
  106. package/packages/remote-control-server/web/dist/assets/{chunk-FMBD7UC4-bszHUkpx.js → chunk-FMBD7UC4-BcO9mQ0P.js} +1 -1
  107. package/packages/remote-control-server/web/dist/assets/{chunk-ND2GUHAM-BVQIhq6J.js → chunk-ND2GUHAM-BvRq13S_.js} +1 -1
  108. package/packages/remote-control-server/web/dist/assets/{chunk-QZHKN3VN-BVEDe5qc.js → chunk-QZHKN3VN-D7GHkPpq.js} +1 -1
  109. package/packages/remote-control-server/web/dist/assets/classDiagram-4FO5ZUOK-pYllpkOq.js +1 -0
  110. package/packages/remote-control-server/web/dist/assets/classDiagram-v2-Q7XG4LA2-pYllpkOq.js +1 -0
  111. package/packages/remote-control-server/web/dist/assets/{code-block-IT6T5CEO-B26n-rFC.js → code-block-IT6T5CEO-B1p-vO-3.js} +1 -1
  112. package/{dist/rcs-web/assets/cose-bilkent-S5V4N54A-DXc7GE8W.js → packages/remote-control-server/web/dist/assets/cose-bilkent-S5V4N54A-L0rUZuH5.js} +1 -1
  113. package/packages/remote-control-server/web/dist/assets/{dagre-BM42HDAG-DAXofHt_.js → dagre-BM42HDAG-CHGuio22.js} +1 -1
  114. package/packages/remote-control-server/web/dist/assets/{diagram-2AECGRRQ-DK0TBUQA.js → diagram-2AECGRRQ-DIDRyQk8.js} +1 -1
  115. package/packages/remote-control-server/web/dist/assets/{diagram-5GNKFQAL-DMsq-Grp.js → diagram-5GNKFQAL-CNubKb6P.js} +1 -1
  116. package/packages/remote-control-server/web/dist/assets/{diagram-KO2AKTUF-B3FMhmUc.js → diagram-KO2AKTUF-CzQh664-.js} +1 -1
  117. package/packages/remote-control-server/web/dist/assets/{diagram-LMA3HP47-BjO3BHFl.js → diagram-LMA3HP47-CWBR7-VG.js} +1 -1
  118. package/packages/remote-control-server/web/dist/assets/{diagram-OG6HWLK6-CNg4B2L3.js → diagram-OG6HWLK6-BMHdlbcZ.js} +1 -1
  119. package/{dist/rcs-web/assets/erDiagram-TEJ5UH35-BDxz8NRK.js → packages/remote-control-server/web/dist/assets/erDiagram-TEJ5UH35-OgT8dC-K.js} +1 -1
  120. package/{dist/rcs-web/assets/flowDiagram-I6XJVG4X-VIgM13sK.js → packages/remote-control-server/web/dist/assets/flowDiagram-I6XJVG4X-DRrCY2oB.js} +1 -1
  121. package/packages/remote-control-server/web/dist/assets/{ganttDiagram-6RSMTGT7-Ba2QuJU0.js → ganttDiagram-6RSMTGT7-DctpzEKN.js} +1 -1
  122. package/packages/remote-control-server/web/dist/assets/{gitGraphDiagram-PVQCEYII-B-gV-lAg.js → gitGraphDiagram-PVQCEYII-5KpLszm9.js} +1 -1
  123. package/packages/remote-control-server/web/dist/assets/{infoDiagram-5YYISTIA-DothwgD4.js → infoDiagram-5YYISTIA-BAKD_Tt7.js} +1 -1
  124. package/{dist/rcs-web/assets/ishikawaDiagram-YF4QCWOH-DhpjM1Vc.js → packages/remote-control-server/web/dist/assets/ishikawaDiagram-YF4QCWOH-VHOPG4Nc.js} +1 -1
  125. package/packages/remote-control-server/web/dist/assets/{journeyDiagram-JHISSGLW-PbQTouMJ.js → journeyDiagram-JHISSGLW-BtGih-ik.js} +1 -1
  126. package/packages/remote-control-server/web/dist/assets/{kanban-definition-UN3LZRKU-63WdwG-X.js → kanban-definition-UN3LZRKU-BsEGZsD1.js} +1 -1
  127. package/packages/remote-control-server/web/dist/assets/{linear-CeRljV9s.js → linear-Dj636PNG.js} +1 -1
  128. package/packages/remote-control-server/web/dist/assets/main-CkqtCllF.js +238 -0
  129. package/packages/remote-control-server/web/dist/assets/{mermaid-VLURNSYL-CG6yNR7g.js → mermaid-VLURNSYL-DutpHHm9.js} +4 -4
  130. package/packages/remote-control-server/web/dist/assets/{mermaid.core-CAuNNATY.js → mermaid.core-XHHf0pen.js} +5 -5
  131. package/packages/remote-control-server/web/dist/assets/{mindmap-definition-RKZ34NQL-9hDL1CUV.js → mindmap-definition-RKZ34NQL-BBa8V_3g.js} +1 -1
  132. package/packages/remote-control-server/web/dist/assets/{pieDiagram-4H26LBE5-BS_P2t04.js → pieDiagram-4H26LBE5-BuNEdo9H.js} +1 -1
  133. package/packages/remote-control-server/web/dist/assets/{quadrantDiagram-W4KKPZXB-Cp9QQkHV.js → quadrantDiagram-W4KKPZXB-CsJgyQIN.js} +1 -1
  134. package/packages/remote-control-server/web/dist/assets/{requirementDiagram-4Y6WPE33-C_XSzOKE.js → requirementDiagram-4Y6WPE33-DqMJZ914.js} +1 -1
  135. package/{dist/rcs-web/assets/sankeyDiagram-5OEKKPKP-BhuVqzZd.js → packages/remote-control-server/web/dist/assets/sankeyDiagram-5OEKKPKP-CVsz09q2.js} +1 -1
  136. package/{dist/rcs-web/assets/sequenceDiagram-3UESZ5HK-C3IPZrcW.js → packages/remote-control-server/web/dist/assets/sequenceDiagram-3UESZ5HK-BRp4Wddn.js} +1 -1
  137. package/packages/remote-control-server/web/dist/assets/{stateDiagram-AJRCARHV-CN4iNPRl.js → stateDiagram-AJRCARHV-S-QqQr-D.js} +1 -1
  138. package/packages/remote-control-server/web/dist/assets/{stateDiagram-v2-BHNVJYJU-CICBlDSJ.js → stateDiagram-v2-BHNVJYJU-D77KfDr-.js} +1 -1
  139. 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
  140. package/packages/remote-control-server/web/dist/assets/{useTeams-DPHGnBHL.js → useTeams-CqJOqEy1.js} +1 -1
  141. package/packages/remote-control-server/web/dist/assets/{vennDiagram-CIIHVFJN-CSaUc2Xv.js → vennDiagram-CIIHVFJN-Dikwrn1L.js} +1 -1
  142. package/packages/remote-control-server/web/dist/assets/{wardleyDiagram-YWT4CUSO-DTuJrl9P.js → wardleyDiagram-YWT4CUSO-DUzpiV6H.js} +1 -1
  143. package/packages/remote-control-server/web/dist/assets/{workerType-CSqeySYe.js → workerType-DOQ0AHrq.js} +1 -1
  144. package/packages/remote-control-server/web/dist/assets/{xychartDiagram-2RQKCTM6-BQ0oU3Qi.js → xychartDiagram-2RQKCTM6-OZNbB32D.js} +1 -1
  145. package/packages/remote-control-server/web/dist/index.html +1 -1
  146. package/dist/rcs-web/assets/Dashboard-D_Im4UId.js +0 -1
  147. package/dist/rcs-web/assets/Join-BTnvMsB9.js +0 -1
  148. package/dist/rcs-web/assets/Login-ClT7RckC.js +0 -1
  149. package/dist/rcs-web/assets/Setup-PaDA6Et-.js +0 -1
  150. package/dist/rcs-web/assets/TeamCreate-249rK3U3.js +0 -1
  151. package/dist/rcs-web/assets/UserSettings-X627Fdni.js +0 -1
  152. package/dist/rcs-web/assets/channel-BUwWbT6U.js +0 -1
  153. package/dist/rcs-web/assets/classDiagram-4FO5ZUOK-DYtx22zv.js +0 -1
  154. package/dist/rcs-web/assets/classDiagram-v2-Q7XG4LA2-DYtx22zv.js +0 -1
  155. package/dist/rcs-web/assets/i18n-Dbt-vEQ3.js +0 -1
  156. package/dist/rcs-web/assets/main-DvfxciBy.js +0 -238
  157. package/packages/remote-control-server/web/dist/assets/Dashboard-D_Im4UId.js +0 -1
  158. package/packages/remote-control-server/web/dist/assets/Join-BTnvMsB9.js +0 -1
  159. package/packages/remote-control-server/web/dist/assets/Login-ClT7RckC.js +0 -1
  160. package/packages/remote-control-server/web/dist/assets/Setup-PaDA6Et-.js +0 -1
  161. package/packages/remote-control-server/web/dist/assets/TeamCreate-249rK3U3.js +0 -1
  162. package/packages/remote-control-server/web/dist/assets/UserSettings-X627Fdni.js +0 -1
  163. package/packages/remote-control-server/web/dist/assets/channel-BUwWbT6U.js +0 -1
  164. package/packages/remote-control-server/web/dist/assets/classDiagram-4FO5ZUOK-DYtx22zv.js +0 -1
  165. package/packages/remote-control-server/web/dist/assets/classDiagram-v2-Q7XG4LA2-DYtx22zv.js +0 -1
  166. package/packages/remote-control-server/web/dist/assets/i18n-Dbt-vEQ3.js +0 -1
  167. 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
- // TODO: Share-based access not yet implemented — currently returns 403 (not owner)
1025
- expect(res.status).toBe(403)
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 cookieHeader = c.req.header('Cookie') || ''
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', false)
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 = hashPassword(password)
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.req.url.startsWith('https://')
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
- if (!user || !verifyPassword(password, user.password_hash)) {
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.req.url.startsWith('https://')
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
- parseCookieValue(c.req.header('Cookie'), 'rcs_refresh')
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.req.url.startsWith('https://')
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 = parseCookieValue(c.req.header('Cookie'), 'rcs_access')
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
- if (invitation.consumed_at || invitation.uses >= invitation.max_uses) {
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' }, 400)
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 AND consumed_at IS NULL` clause
405
- // ensures that concurrent joins for a max_uses=1 invitation only
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 = hashPassword(password)
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 * FROM invitations WHERE token_hash = $hash')
416
- .get({ $hash: tokenHash }) as typeof invitation | null
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
- db.query(
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, consumed_at = $now, consumed_by = $userId
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 (result.changes === 0) {
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.req.url.startsWith('https://')
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
- /** POST /web/bind — Bind a session to a UUID (no-login auth) */
11
- app.post('/bind', async c => {
12
- const body = await c.req.json()
13
- const sessionId = body.sessionId
14
- // UUID can come from query param (api.js sends it in URL) or body
15
- const uuid = c.req.query('uuid') || body.uuid
16
-
17
- if (!sessionId || !uuid) {
18
- return c.json({ error: 'sessionId and uuid are required' }, 400)
19
- }
20
-
21
- const resolvedSessionId = resolveExistingWebSessionId(sessionId)
22
- if (!resolvedSessionId) {
23
- return c.json({ error: 'Session not found' }, 404)
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 uuid = c.get('userId')!
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
- storeBindSession(sessionId, uuid)
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
- storeBindSession(session.id, uuid)
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' } },