@saluzi/saluzi-edu 0.2.9 → 0.2.11

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 (219) hide show
  1. package/dist/cli.js +336 -333
  2. package/dist/rcs-web/assets/Dashboard-BwotBWlJ.js +1 -0
  3. package/dist/rcs-web/assets/Join-4yrksbdX.js +1 -0
  4. package/dist/rcs-web/assets/Login-Cxm_yEeE.js +1 -0
  5. package/dist/rcs-web/assets/SessionDetail-DNdfPOYy.js +50 -0
  6. package/dist/rcs-web/assets/Setup-BiAAxugt.js +1 -0
  7. package/dist/rcs-web/assets/ShareLanding-CP3Z6ckr.js +1 -0
  8. package/dist/rcs-web/assets/TeamCreate-DgdCwRSb.js +1 -0
  9. package/dist/rcs-web/assets/TeamDetail-CkNTFoMB.js +11 -0
  10. package/dist/rcs-web/assets/TeamList-Bf0sQ5RQ.js +1 -0
  11. package/dist/rcs-web/assets/TeamSettings-BaNAfXU3.js +1 -0
  12. package/dist/rcs-web/assets/UserSettings-CsutQjxG.js +1 -0
  13. package/{packages/remote-control-server/web/dist/assets/arc-D90597o-.js → dist/rcs-web/assets/arc-BxxmNQO6.js} +1 -1
  14. package/dist/rcs-web/assets/architectureDiagram-3BPJPVTR-DBjkRdbd.js +36 -0
  15. package/dist/rcs-web/assets/{blockDiagram-GPEHLZMM-DHd8IbOs.js → blockDiagram-GPEHLZMM-BF6COWlM.js} +2 -2
  16. package/dist/rcs-web/assets/{c4Diagram-AAUBKEIU-CjFkQJ9h.js → c4Diagram-AAUBKEIU-DHOf390v.js} +2 -2
  17. package/dist/rcs-web/assets/channel-JsvbMQjW.js +1 -0
  18. package/dist/rcs-web/assets/{chunk-2J33WTMH-C21XWnGZ.js → chunk-2J33WTMH-CeJN8yjM.js} +1 -1
  19. package/dist/rcs-web/assets/{chunk-4BX2VUAB-B3SXiJlt.js → chunk-4BX2VUAB-jP76_P2A.js} +1 -1
  20. package/dist/rcs-web/assets/{chunk-55IACEB6-DjImv45F.js → chunk-55IACEB6-BsFA6emB.js} +1 -1
  21. package/{packages/remote-control-server/web/dist/assets/chunk-727SXJPM-DtnaqsyD.js → dist/rcs-web/assets/chunk-727SXJPM-BF4HrQjZ.js} +1 -1
  22. package/dist/rcs-web/assets/{chunk-AQP2D5EJ-BVFgH5mS.js → chunk-AQP2D5EJ-DToJ74Kc.js} +1 -1
  23. package/dist/rcs-web/assets/{chunk-FMBD7UC4-Bl8jgb57.js → chunk-FMBD7UC4-DVTARgsa.js} +1 -1
  24. package/dist/rcs-web/assets/{chunk-ND2GUHAM-BpK111zh.js → chunk-ND2GUHAM-C50Gqv9c.js} +1 -1
  25. package/dist/rcs-web/assets/{chunk-QZHKN3VN-ruhIHBim.js → chunk-QZHKN3VN-EsIIh6zV.js} +1 -1
  26. package/dist/rcs-web/assets/classDiagram-4FO5ZUOK-BXn8Uc4c.js +1 -0
  27. package/dist/rcs-web/assets/classDiagram-v2-Q7XG4LA2-BXn8Uc4c.js +1 -0
  28. package/dist/rcs-web/assets/code-block-IT6T5CEO-BnufHNTT.js +2 -0
  29. package/dist/rcs-web/assets/cose-bilkent-S5V4N54A-M6tNt14I.js +1 -0
  30. package/dist/rcs-web/assets/dagre-BM42HDAG-DaTm29qG.js +4 -0
  31. package/dist/rcs-web/assets/diagram-2AECGRRQ-fqoeOvq2.js +43 -0
  32. package/dist/rcs-web/assets/diagram-5GNKFQAL-6s2CiaGG.js +10 -0
  33. package/dist/rcs-web/assets/{diagram-KO2AKTUF-Uy3PDP5S.js → diagram-KO2AKTUF-C0aw_N95.js} +2 -2
  34. package/dist/rcs-web/assets/diagram-LMA3HP47-C8_Rv3rB.js +24 -0
  35. package/dist/rcs-web/assets/diagram-OG6HWLK6-Dae4u8Uc.js +24 -0
  36. package/{packages/remote-control-server/web/dist/assets/erDiagram-TEJ5UH35-sre_MTeK.js → dist/rcs-web/assets/erDiagram-TEJ5UH35-CosS5VWr.js} +13 -13
  37. package/dist/rcs-web/assets/{flowDiagram-I6XJVG4X-DwrWaLoe.js → flowDiagram-I6XJVG4X-D_Mxs46-.js} +2 -2
  38. package/dist/rcs-web/assets/ganttDiagram-6RSMTGT7-B0jy8Am_.js +292 -0
  39. package/dist/rcs-web/assets/gitGraphDiagram-PVQCEYII-CSdu2HVI.js +106 -0
  40. package/dist/rcs-web/assets/i18n-Dbt-vEQ3.js +1 -0
  41. package/dist/rcs-web/assets/infoDiagram-5YYISTIA-Dcbc9L1S.js +2 -0
  42. package/{packages/remote-control-server/web/dist/assets/ishikawaDiagram-YF4QCWOH-SO0nn3--.js → dist/rcs-web/assets/ishikawaDiagram-YF4QCWOH-BLMjFE4t.js} +2 -2
  43. package/dist/rcs-web/assets/{journeyDiagram-JHISSGLW-D4igFUd4.js → journeyDiagram-JHISSGLW-Dw4nh1xI.js} +2 -2
  44. package/dist/rcs-web/assets/{kanban-definition-UN3LZRKU-Cm_yKsZC.js → kanban-definition-UN3LZRKU-DkU3j_ly.js} +2 -2
  45. package/dist/rcs-web/assets/{linear-4IMGll_E.js → linear-N5Cify7M.js} +1 -1
  46. package/dist/rcs-web/assets/main-30YqUvTd.js +238 -0
  47. package/dist/rcs-web/assets/main-EP6Lac2m.css +1 -0
  48. package/{packages/remote-control-server/web/dist/assets/mermaid-VLURNSYL-D_yQ7nAd.js → dist/rcs-web/assets/mermaid-VLURNSYL-gfiJUpl_.js} +5 -5
  49. package/dist/rcs-web/assets/{mermaid.core-B1qOUdiU.js → mermaid.core-B_Xc65Bf.js} +5 -5
  50. package/dist/rcs-web/assets/{mindmap-definition-RKZ34NQL-yETom5qt.js → mindmap-definition-RKZ34NQL-DUavHfNu.js} +2 -2
  51. package/{packages/remote-control-server/web/dist/assets/pieDiagram-4H26LBE5-KIhqXGT6.js → dist/rcs-web/assets/pieDiagram-4H26LBE5-DSJaToTU.js} +2 -2
  52. package/{packages/remote-control-server/web/dist/assets/quadrantDiagram-W4KKPZXB-DISqmoA6.js → dist/rcs-web/assets/quadrantDiagram-W4KKPZXB-BFp6TnHj.js} +2 -2
  53. package/dist/rcs-web/assets/{requirementDiagram-4Y6WPE33-DsgT4ptk.js → requirementDiagram-4Y6WPE33-eil6F3-O.js} +2 -2
  54. package/{packages/remote-control-server/web/dist/assets/sankeyDiagram-5OEKKPKP-Dn67kbn0.js → dist/rcs-web/assets/sankeyDiagram-5OEKKPKP-DbGhoepK.js} +2 -2
  55. package/{packages/remote-control-server/web/dist/assets/sequenceDiagram-3UESZ5HK-CtWaXZHl.js → dist/rcs-web/assets/sequenceDiagram-3UESZ5HK-B0YrN1ud.js} +14 -14
  56. package/dist/rcs-web/assets/stateDiagram-AJRCARHV-CofFPj3N.js +1 -0
  57. package/dist/rcs-web/assets/stateDiagram-v2-BHNVJYJU-DPlcvPn1.js +1 -0
  58. package/{packages/remote-control-server/web/dist/assets/timeline-definition-PNZ67QCA-Dk5WUenC.js → dist/rcs-web/assets/timeline-definition-PNZ67QCA-Bx4dusy8.js} +2 -2
  59. package/dist/rcs-web/assets/useTeams-MDI4djXg.js +1 -0
  60. package/{packages/remote-control-server/web/dist/assets/vennDiagram-CIIHVFJN-TY8EiRrC.js → dist/rcs-web/assets/vennDiagram-CIIHVFJN-B_-3l-MF.js} +2 -2
  61. package/dist/rcs-web/assets/{wardleyDiagram-YWT4CUSO-BK3oi47X.js → wardleyDiagram-YWT4CUSO-DNSn-pGp.js} +2 -2
  62. package/dist/rcs-web/assets/workerType-OM09y7Lk.js +1 -0
  63. package/{packages/remote-control-server/web/dist/assets/xychartDiagram-2RQKCTM6-DhsxqX67.js → dist/rcs-web/assets/xychartDiagram-2RQKCTM6-TqUbNp0j.js} +2 -2
  64. package/dist/rcs-web/index.html +2 -3
  65. package/package.json +2 -1
  66. package/packages/remote-control-server/src/__tests__/acp-ws-handler.test.ts +7 -19
  67. package/packages/remote-control-server/src/__tests__/auth-login.test.ts +561 -0
  68. package/packages/remote-control-server/src/__tests__/auth-security.test.ts +547 -0
  69. package/packages/remote-control-server/src/__tests__/auth.test.ts +7 -18
  70. package/packages/remote-control-server/src/__tests__/bind-replacement.test.ts +251 -0
  71. package/packages/remote-control-server/src/__tests__/cookie.test.ts +155 -0
  72. package/packages/remote-control-server/src/__tests__/data-layer-fixes.test.ts +147 -0
  73. package/packages/remote-control-server/src/__tests__/db.test.ts +582 -0
  74. package/packages/remote-control-server/src/__tests__/design-gaps.test.ts +440 -0
  75. package/packages/remote-control-server/src/__tests__/disconnect-monitor.test.ts +2 -18
  76. package/packages/remote-control-server/src/__tests__/helpers/db.ts +43 -0
  77. package/packages/remote-control-server/src/__tests__/helpers/fixtures.ts +94 -0
  78. package/packages/remote-control-server/src/__tests__/helpers/mock-config.ts +31 -0
  79. package/packages/remote-control-server/src/__tests__/middleware.test.ts +4 -112
  80. package/packages/remote-control-server/src/__tests__/password.test.ts +76 -0
  81. package/packages/remote-control-server/src/__tests__/phase4-cleanup.test.ts +290 -0
  82. package/packages/remote-control-server/src/__tests__/phase4-integration.test.ts +224 -0
  83. package/packages/remote-control-server/src/__tests__/routes.test.ts +292 -207
  84. package/packages/remote-control-server/src/__tests__/services.test.ts +2 -18
  85. package/packages/remote-control-server/src/__tests__/session-visibility.test.ts +452 -0
  86. package/packages/remote-control-server/src/__tests__/session.test.ts +216 -0
  87. package/packages/remote-control-server/src/__tests__/shares.test.ts +1419 -0
  88. package/packages/remote-control-server/src/__tests__/sse-writer.test.ts +2 -18
  89. package/packages/remote-control-server/src/__tests__/teams.test.ts +2089 -0
  90. package/packages/remote-control-server/src/__tests__/work-dispatch.test.ts +2 -18
  91. package/packages/remote-control-server/src/__tests__/worker-token-removal.test.ts +487 -0
  92. package/packages/remote-control-server/src/__tests__/ws-handler.test.ts +2 -18
  93. package/packages/remote-control-server/src/auth/constants.ts +13 -0
  94. package/packages/remote-control-server/src/auth/cookie.ts +75 -0
  95. package/packages/remote-control-server/src/auth/middleware.ts +75 -165
  96. package/packages/remote-control-server/src/auth/password.ts +116 -0
  97. package/packages/remote-control-server/src/auth/session.ts +130 -0
  98. package/packages/remote-control-server/src/db/schema.sql +116 -0
  99. package/packages/remote-control-server/src/db/sqlite.ts +99 -0
  100. package/packages/remote-control-server/src/routes/acp/index.ts +6 -11
  101. package/packages/remote-control-server/src/routes/v1/environments.ts +111 -8
  102. package/packages/remote-control-server/src/routes/web/auth-routes.ts +634 -0
  103. package/packages/remote-control-server/src/routes/web/control.ts +5 -5
  104. package/packages/remote-control-server/src/routes/web/environments.ts +5 -27
  105. package/packages/remote-control-server/src/routes/web/sessions.ts +19 -41
  106. package/packages/remote-control-server/src/routes/web/shares.ts +371 -0
  107. package/packages/remote-control-server/src/routes/web/teams.ts +850 -0
  108. package/packages/remote-control-server/src/services/environment.ts +1 -4
  109. package/packages/remote-control-server/src/services/session-visibility.ts +79 -0
  110. package/packages/remote-control-server/src/services/session.ts +0 -13
  111. package/packages/remote-control-server/src/store.ts +150 -56
  112. package/packages/remote-control-server/src/types/api.ts +2 -8
  113. package/packages/remote-control-server/web/dist/assets/Dashboard-BwotBWlJ.js +1 -0
  114. package/packages/remote-control-server/web/dist/assets/Join-4yrksbdX.js +1 -0
  115. package/packages/remote-control-server/web/dist/assets/Login-Cxm_yEeE.js +1 -0
  116. package/packages/remote-control-server/web/dist/assets/SessionDetail-DNdfPOYy.js +50 -0
  117. package/packages/remote-control-server/web/dist/assets/Setup-BiAAxugt.js +1 -0
  118. package/packages/remote-control-server/web/dist/assets/ShareLanding-CP3Z6ckr.js +1 -0
  119. package/packages/remote-control-server/web/dist/assets/TeamCreate-DgdCwRSb.js +1 -0
  120. package/packages/remote-control-server/web/dist/assets/TeamDetail-CkNTFoMB.js +11 -0
  121. package/packages/remote-control-server/web/dist/assets/TeamList-Bf0sQ5RQ.js +1 -0
  122. package/packages/remote-control-server/web/dist/assets/TeamSettings-BaNAfXU3.js +1 -0
  123. package/packages/remote-control-server/web/dist/assets/UserSettings-CsutQjxG.js +1 -0
  124. package/{dist/rcs-web/assets/arc-D90597o-.js → packages/remote-control-server/web/dist/assets/arc-BxxmNQO6.js} +1 -1
  125. package/packages/remote-control-server/web/dist/assets/architectureDiagram-3BPJPVTR-DBjkRdbd.js +36 -0
  126. package/packages/remote-control-server/web/dist/assets/{blockDiagram-GPEHLZMM-DHd8IbOs.js → blockDiagram-GPEHLZMM-BF6COWlM.js} +2 -2
  127. package/packages/remote-control-server/web/dist/assets/{c4Diagram-AAUBKEIU-CjFkQJ9h.js → c4Diagram-AAUBKEIU-DHOf390v.js} +2 -2
  128. package/packages/remote-control-server/web/dist/assets/channel-JsvbMQjW.js +1 -0
  129. package/packages/remote-control-server/web/dist/assets/{chunk-2J33WTMH-C21XWnGZ.js → chunk-2J33WTMH-CeJN8yjM.js} +1 -1
  130. package/packages/remote-control-server/web/dist/assets/{chunk-4BX2VUAB-B3SXiJlt.js → chunk-4BX2VUAB-jP76_P2A.js} +1 -1
  131. package/packages/remote-control-server/web/dist/assets/{chunk-55IACEB6-DjImv45F.js → chunk-55IACEB6-BsFA6emB.js} +1 -1
  132. package/{dist/rcs-web/assets/chunk-727SXJPM-DtnaqsyD.js → packages/remote-control-server/web/dist/assets/chunk-727SXJPM-BF4HrQjZ.js} +1 -1
  133. package/packages/remote-control-server/web/dist/assets/{chunk-AQP2D5EJ-BVFgH5mS.js → chunk-AQP2D5EJ-DToJ74Kc.js} +1 -1
  134. package/packages/remote-control-server/web/dist/assets/{chunk-FMBD7UC4-Bl8jgb57.js → chunk-FMBD7UC4-DVTARgsa.js} +1 -1
  135. package/packages/remote-control-server/web/dist/assets/{chunk-ND2GUHAM-BpK111zh.js → chunk-ND2GUHAM-C50Gqv9c.js} +1 -1
  136. package/packages/remote-control-server/web/dist/assets/{chunk-QZHKN3VN-ruhIHBim.js → chunk-QZHKN3VN-EsIIh6zV.js} +1 -1
  137. package/packages/remote-control-server/web/dist/assets/classDiagram-4FO5ZUOK-BXn8Uc4c.js +1 -0
  138. package/packages/remote-control-server/web/dist/assets/classDiagram-v2-Q7XG4LA2-BXn8Uc4c.js +1 -0
  139. package/packages/remote-control-server/web/dist/assets/code-block-IT6T5CEO-BnufHNTT.js +2 -0
  140. package/packages/remote-control-server/web/dist/assets/cose-bilkent-S5V4N54A-M6tNt14I.js +1 -0
  141. package/packages/remote-control-server/web/dist/assets/dagre-BM42HDAG-DaTm29qG.js +4 -0
  142. package/packages/remote-control-server/web/dist/assets/diagram-2AECGRRQ-fqoeOvq2.js +43 -0
  143. package/packages/remote-control-server/web/dist/assets/diagram-5GNKFQAL-6s2CiaGG.js +10 -0
  144. package/packages/remote-control-server/web/dist/assets/{diagram-KO2AKTUF-Uy3PDP5S.js → diagram-KO2AKTUF-C0aw_N95.js} +2 -2
  145. package/packages/remote-control-server/web/dist/assets/diagram-LMA3HP47-C8_Rv3rB.js +24 -0
  146. package/packages/remote-control-server/web/dist/assets/diagram-OG6HWLK6-Dae4u8Uc.js +24 -0
  147. package/{dist/rcs-web/assets/erDiagram-TEJ5UH35-sre_MTeK.js → packages/remote-control-server/web/dist/assets/erDiagram-TEJ5UH35-CosS5VWr.js} +13 -13
  148. package/packages/remote-control-server/web/dist/assets/{flowDiagram-I6XJVG4X-DwrWaLoe.js → flowDiagram-I6XJVG4X-D_Mxs46-.js} +2 -2
  149. package/packages/remote-control-server/web/dist/assets/ganttDiagram-6RSMTGT7-B0jy8Am_.js +292 -0
  150. package/packages/remote-control-server/web/dist/assets/gitGraphDiagram-PVQCEYII-CSdu2HVI.js +106 -0
  151. package/packages/remote-control-server/web/dist/assets/i18n-Dbt-vEQ3.js +1 -0
  152. package/packages/remote-control-server/web/dist/assets/infoDiagram-5YYISTIA-Dcbc9L1S.js +2 -0
  153. package/{dist/rcs-web/assets/ishikawaDiagram-YF4QCWOH-SO0nn3--.js → packages/remote-control-server/web/dist/assets/ishikawaDiagram-YF4QCWOH-BLMjFE4t.js} +2 -2
  154. package/packages/remote-control-server/web/dist/assets/{journeyDiagram-JHISSGLW-D4igFUd4.js → journeyDiagram-JHISSGLW-Dw4nh1xI.js} +2 -2
  155. package/packages/remote-control-server/web/dist/assets/{kanban-definition-UN3LZRKU-Cm_yKsZC.js → kanban-definition-UN3LZRKU-DkU3j_ly.js} +2 -2
  156. package/packages/remote-control-server/web/dist/assets/{linear-4IMGll_E.js → linear-N5Cify7M.js} +1 -1
  157. package/packages/remote-control-server/web/dist/assets/main-30YqUvTd.js +238 -0
  158. package/packages/remote-control-server/web/dist/assets/main-EP6Lac2m.css +1 -0
  159. package/{dist/rcs-web/assets/mermaid-VLURNSYL-D_yQ7nAd.js → packages/remote-control-server/web/dist/assets/mermaid-VLURNSYL-gfiJUpl_.js} +5 -5
  160. package/packages/remote-control-server/web/dist/assets/{mermaid.core-B1qOUdiU.js → mermaid.core-B_Xc65Bf.js} +5 -5
  161. package/packages/remote-control-server/web/dist/assets/{mindmap-definition-RKZ34NQL-yETom5qt.js → mindmap-definition-RKZ34NQL-DUavHfNu.js} +2 -2
  162. package/{dist/rcs-web/assets/pieDiagram-4H26LBE5-KIhqXGT6.js → packages/remote-control-server/web/dist/assets/pieDiagram-4H26LBE5-DSJaToTU.js} +2 -2
  163. package/{dist/rcs-web/assets/quadrantDiagram-W4KKPZXB-DISqmoA6.js → packages/remote-control-server/web/dist/assets/quadrantDiagram-W4KKPZXB-BFp6TnHj.js} +2 -2
  164. package/packages/remote-control-server/web/dist/assets/{requirementDiagram-4Y6WPE33-DsgT4ptk.js → requirementDiagram-4Y6WPE33-eil6F3-O.js} +2 -2
  165. package/{dist/rcs-web/assets/sankeyDiagram-5OEKKPKP-Dn67kbn0.js → packages/remote-control-server/web/dist/assets/sankeyDiagram-5OEKKPKP-DbGhoepK.js} +2 -2
  166. package/{dist/rcs-web/assets/sequenceDiagram-3UESZ5HK-CtWaXZHl.js → packages/remote-control-server/web/dist/assets/sequenceDiagram-3UESZ5HK-B0YrN1ud.js} +14 -14
  167. package/packages/remote-control-server/web/dist/assets/stateDiagram-AJRCARHV-CofFPj3N.js +1 -0
  168. package/packages/remote-control-server/web/dist/assets/stateDiagram-v2-BHNVJYJU-DPlcvPn1.js +1 -0
  169. package/{dist/rcs-web/assets/timeline-definition-PNZ67QCA-Dk5WUenC.js → packages/remote-control-server/web/dist/assets/timeline-definition-PNZ67QCA-Bx4dusy8.js} +2 -2
  170. package/packages/remote-control-server/web/dist/assets/useTeams-MDI4djXg.js +1 -0
  171. package/{dist/rcs-web/assets/vennDiagram-CIIHVFJN-TY8EiRrC.js → packages/remote-control-server/web/dist/assets/vennDiagram-CIIHVFJN-B_-3l-MF.js} +2 -2
  172. package/packages/remote-control-server/web/dist/assets/{wardleyDiagram-YWT4CUSO-BK3oi47X.js → wardleyDiagram-YWT4CUSO-DNSn-pGp.js} +2 -2
  173. package/packages/remote-control-server/web/dist/assets/workerType-OM09y7Lk.js +1 -0
  174. package/{dist/rcs-web/assets/xychartDiagram-2RQKCTM6-DhsxqX67.js → packages/remote-control-server/web/dist/assets/xychartDiagram-2RQKCTM6-TqUbNp0j.js} +2 -2
  175. package/packages/remote-control-server/web/dist/index.html +2 -3
  176. package/dist/rcs-web/assets/Dashboard-Cyj-Bb86.js +0 -2
  177. package/dist/rcs-web/assets/SessionDetail-SA9QBkyj.js +0 -51
  178. package/dist/rcs-web/assets/architectureDiagram-3BPJPVTR-CG9q9_bk.js +0 -36
  179. package/dist/rcs-web/assets/channel-CQFGglg-.js +0 -1
  180. package/dist/rcs-web/assets/classDiagram-4FO5ZUOK-C5rBdShD.js +0 -1
  181. package/dist/rcs-web/assets/classDiagram-v2-Q7XG4LA2-C5rBdShD.js +0 -1
  182. package/dist/rcs-web/assets/code-block-IT6T5CEO-BLGFrBw3.js +0 -2
  183. package/dist/rcs-web/assets/cose-bilkent-S5V4N54A-DBKEXjqM.js +0 -1
  184. package/dist/rcs-web/assets/dagre-BM42HDAG-DB_SWlAe.js +0 -4
  185. package/dist/rcs-web/assets/diagram-2AECGRRQ-Gbm8G3Xz.js +0 -43
  186. package/dist/rcs-web/assets/diagram-5GNKFQAL-BBuWqfDc.js +0 -10
  187. package/dist/rcs-web/assets/diagram-LMA3HP47-DyUwNrlg.js +0 -24
  188. package/dist/rcs-web/assets/diagram-OG6HWLK6-BXpvRwkJ.js +0 -24
  189. package/dist/rcs-web/assets/ganttDiagram-6RSMTGT7-ByDYviDl.js +0 -292
  190. package/dist/rcs-web/assets/gitGraphDiagram-PVQCEYII-BUnOPw74.js +0 -106
  191. package/dist/rcs-web/assets/infoDiagram-5YYISTIA-B1RpwJHG.js +0 -2
  192. package/dist/rcs-web/assets/main-0A4a3FDU.css +0 -1
  193. package/dist/rcs-web/assets/main-Dom16YJk.js +0 -283
  194. package/dist/rcs-web/assets/qr-C0jP9HcD.js +0 -8
  195. package/dist/rcs-web/assets/qr-scanner-worker.min-D85Z9gVD.js +0 -98
  196. package/dist/rcs-web/assets/stateDiagram-AJRCARHV-D6xp64f3.js +0 -1
  197. package/dist/rcs-web/assets/stateDiagram-v2-BHNVJYJU-D76vZCyQ.js +0 -1
  198. package/packages/remote-control-server/web/dist/assets/Dashboard-Cyj-Bb86.js +0 -2
  199. package/packages/remote-control-server/web/dist/assets/SessionDetail-SA9QBkyj.js +0 -51
  200. package/packages/remote-control-server/web/dist/assets/architectureDiagram-3BPJPVTR-CG9q9_bk.js +0 -36
  201. package/packages/remote-control-server/web/dist/assets/channel-CQFGglg-.js +0 -1
  202. package/packages/remote-control-server/web/dist/assets/classDiagram-4FO5ZUOK-C5rBdShD.js +0 -1
  203. package/packages/remote-control-server/web/dist/assets/classDiagram-v2-Q7XG4LA2-C5rBdShD.js +0 -1
  204. package/packages/remote-control-server/web/dist/assets/code-block-IT6T5CEO-BLGFrBw3.js +0 -2
  205. package/packages/remote-control-server/web/dist/assets/cose-bilkent-S5V4N54A-DBKEXjqM.js +0 -1
  206. package/packages/remote-control-server/web/dist/assets/dagre-BM42HDAG-DB_SWlAe.js +0 -4
  207. package/packages/remote-control-server/web/dist/assets/diagram-2AECGRRQ-Gbm8G3Xz.js +0 -43
  208. package/packages/remote-control-server/web/dist/assets/diagram-5GNKFQAL-BBuWqfDc.js +0 -10
  209. package/packages/remote-control-server/web/dist/assets/diagram-LMA3HP47-DyUwNrlg.js +0 -24
  210. package/packages/remote-control-server/web/dist/assets/diagram-OG6HWLK6-BXpvRwkJ.js +0 -24
  211. package/packages/remote-control-server/web/dist/assets/ganttDiagram-6RSMTGT7-ByDYviDl.js +0 -292
  212. package/packages/remote-control-server/web/dist/assets/gitGraphDiagram-PVQCEYII-BUnOPw74.js +0 -106
  213. package/packages/remote-control-server/web/dist/assets/infoDiagram-5YYISTIA-B1RpwJHG.js +0 -2
  214. package/packages/remote-control-server/web/dist/assets/main-0A4a3FDU.css +0 -1
  215. package/packages/remote-control-server/web/dist/assets/main-Dom16YJk.js +0 -283
  216. package/packages/remote-control-server/web/dist/assets/qr-C0jP9HcD.js +0 -8
  217. package/packages/remote-control-server/web/dist/assets/qr-scanner-worker.min-D85Z9gVD.js +0 -98
  218. package/packages/remote-control-server/web/dist/assets/stateDiagram-AJRCARHV-D6xp64f3.js +0 -1
  219. package/packages/remote-control-server/web/dist/assets/stateDiagram-v2-BHNVJYJU-D76vZCyQ.js +0 -1
@@ -0,0 +1,850 @@
1
+ import { Hono } from 'hono'
2
+ import { randomUUID, createHash } from 'node:crypto'
3
+ import { getDb } from '../../db/sqlite'
4
+ import { sessionAuth } from '../../auth/middleware'
5
+ import { SYSTEM_USER_ID } from '../../auth/constants'
6
+
7
+ const app = new Hono()
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Helpers
11
+ // ---------------------------------------------------------------------------
12
+
13
+ const SLUG_RE = /^[a-z0-9-]{3,32}$/
14
+
15
+ function isValidSlug(slug: string): boolean {
16
+ return SLUG_RE.test(slug)
17
+ }
18
+
19
+ interface UserInfo {
20
+ userId: string
21
+ isAdmin: boolean
22
+ role: string
23
+ }
24
+
25
+ /**
26
+ * Resolve the current user's info from context.
27
+ * System admin (via API key bypass) gets userId=SYSTEM_USER_ID.
28
+ * Also checks DB role for users who logged in as admin.
29
+ */
30
+ function getUserInfo(c: import('hono').Context): UserInfo {
31
+ const userId = c.get('userId') as string
32
+ const isAdminCtx = c.get('isAdmin') as boolean
33
+
34
+ if (userId === SYSTEM_USER_ID || isAdminCtx) {
35
+ return { userId: SYSTEM_USER_ID, isAdmin: true, role: 'admin' }
36
+ }
37
+
38
+ // Look up user role from DB
39
+ const db = getDb()
40
+ const row = db
41
+ .query('SELECT role FROM users WHERE id = $id')
42
+ .get({ $id: userId }) as { role: string } | null
43
+
44
+ const role = row?.role || 'guest'
45
+
46
+ return {
47
+ userId,
48
+ isAdmin: role === 'admin',
49
+ role,
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Get the user's role within a specific team.
55
+ * Returns null if user is not a member.
56
+ * System admins get 'owner' implicitly.
57
+ */
58
+ function getTeamRole(teamId: string, user: UserInfo): string | null {
59
+ if (user.isAdmin) return 'owner'
60
+
61
+ const db = getDb()
62
+ const row = db
63
+ .query(
64
+ 'SELECT role FROM team_members WHERE team_id = $teamId AND user_id = $userId',
65
+ )
66
+ .get({ $teamId: teamId, $userId: user.userId }) as {
67
+ role: string
68
+ } | null
69
+
70
+ return row?.role || null
71
+ }
72
+
73
+ function getTeamOr404(c: import('hono').Context, teamId: string) {
74
+ const db = getDb()
75
+ const team = db
76
+ .query('SELECT * FROM teams WHERE id = $id AND deleted_at IS NULL')
77
+ .get({ $id: teamId }) as Record<string, unknown> | null
78
+ if (!team) {
79
+ return { error: c.json({ error: 'Team not found' }, 404) }
80
+ }
81
+ return { team }
82
+ }
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Auth middleware wrapper
86
+ // ---------------------------------------------------------------------------
87
+
88
+ async function withAuth(c: import('hono').Context, next: import('hono').Next) {
89
+ const result = await sessionAuth(c, next)
90
+ if (result) return result
91
+ }
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // POST /teams — Create team
95
+ // ---------------------------------------------------------------------------
96
+
97
+ app.post('/teams', withAuth, async c => {
98
+ const user = getUserInfo(c)
99
+
100
+ // Guest cannot create teams
101
+ if (!user.isAdmin && user.role === 'guest') {
102
+ return c.json({ error: 'Guests cannot create teams' }, 403)
103
+ }
104
+
105
+ let body: Record<string, unknown>
106
+ try {
107
+ body = await c.req.json()
108
+ } catch {
109
+ return c.json({ error: 'Invalid JSON' }, 400)
110
+ }
111
+
112
+ const { name, slug, description } = body as {
113
+ name?: string
114
+ slug?: string
115
+ description?: string
116
+ }
117
+
118
+ if (!name || typeof name !== 'string') {
119
+ return c.json({ error: 'Name is required' }, 400)
120
+ }
121
+ if (!slug || typeof slug !== 'string') {
122
+ return c.json({ error: 'Slug is required' }, 400)
123
+ }
124
+ if (!isValidSlug(slug)) {
125
+ return c.json(
126
+ {
127
+ error:
128
+ 'Invalid slug: must be 3-32 lowercase letters, digits, or hyphens',
129
+ },
130
+ 400,
131
+ )
132
+ }
133
+
134
+ const db = getDb()
135
+
136
+ // Check slug uniqueness
137
+ const existing = db
138
+ .query('SELECT id FROM teams WHERE slug = $slug AND deleted_at IS NULL')
139
+ .get({ $slug: slug })
140
+ if (existing) {
141
+ return c.json({ error: 'Slug already taken' }, 409)
142
+ }
143
+
144
+ const teamId = `team_${randomUUID().replace(/-/g, '')}`
145
+ const now = new Date().toISOString()
146
+
147
+ // For system admin, use the first admin user as created_by
148
+ let createdBy = user.userId
149
+ if (user.isAdmin) {
150
+ const adminUser = db
151
+ .query("SELECT id FROM users WHERE role = 'admin' LIMIT 1")
152
+ .get() as { id: string } | null
153
+ createdBy = adminUser?.id || user.userId
154
+ }
155
+
156
+ db.query(
157
+ `INSERT INTO teams (id, name, slug, description, created_by, created_at, updated_at)
158
+ VALUES ($id, $name, $slug, $desc, $by, $now, $now)`,
159
+ ).run({
160
+ $id: teamId,
161
+ $name: name,
162
+ $slug: slug,
163
+ $desc: description || null,
164
+ $by: createdBy,
165
+ $now: now,
166
+ })
167
+
168
+ // Creator becomes owner
169
+ db.query(
170
+ `INSERT INTO team_members (team_id, user_id, role, added_at, added_by)
171
+ VALUES ($teamId, $userId, 'owner', $now, $addedBy)`,
172
+ ).run({ $teamId: teamId, $userId: createdBy, $addedBy: createdBy, $now: now })
173
+
174
+ return c.json(
175
+ {
176
+ id: teamId,
177
+ name,
178
+ slug,
179
+ description: description || null,
180
+ created_by: createdBy,
181
+ created_at: now,
182
+ updated_at: now,
183
+ },
184
+ 201,
185
+ )
186
+ })
187
+
188
+ // ---------------------------------------------------------------------------
189
+ // GET /teams — List teams
190
+ // ---------------------------------------------------------------------------
191
+
192
+ app.get('/teams', withAuth, async c => {
193
+ const user = getUserInfo(c)
194
+ const db = getDb()
195
+
196
+ if (user.isAdmin) {
197
+ // Admin sees all non-deleted teams
198
+ const teams = db
199
+ .query('SELECT * FROM teams WHERE deleted_at IS NULL ORDER BY name')
200
+ .all()
201
+ return c.json(teams, 200)
202
+ }
203
+
204
+ if (user.role === 'guest') {
205
+ return c.json([], 200)
206
+ }
207
+
208
+ // Member sees only teams they belong to
209
+ const teams = db
210
+ .query(
211
+ `SELECT t.* FROM teams t
212
+ JOIN team_members tm ON tm.team_id = t.id
213
+ WHERE tm.user_id = $userId AND t.deleted_at IS NULL
214
+ ORDER BY t.name`,
215
+ )
216
+ .all({ $userId: user.userId })
217
+
218
+ return c.json(teams, 200)
219
+ })
220
+
221
+ // ---------------------------------------------------------------------------
222
+ // GET /teams/:id — Get team details
223
+ // ---------------------------------------------------------------------------
224
+
225
+ app.get('/teams/:id', withAuth, async c => {
226
+ const teamId = c.req.param('id')!
227
+ const user = getUserInfo(c)
228
+ const result = getTeamOr404(c, teamId)
229
+ if ('error' in result) return result.error
230
+
231
+ const teamRole = getTeamRole(teamId, user)
232
+ if (!teamRole) {
233
+ return c.json({ error: 'Not a team member' }, 403)
234
+ }
235
+
236
+ return c.json(result.team, 200)
237
+ })
238
+
239
+ // ---------------------------------------------------------------------------
240
+ // PATCH /teams/:id — Update team
241
+ // ---------------------------------------------------------------------------
242
+
243
+ app.patch('/teams/:id', withAuth, async c => {
244
+ const teamId = c.req.param('id')!
245
+ const user = getUserInfo(c)
246
+ const result = getTeamOr404(c, teamId)
247
+ if ('error' in result) return result.error
248
+
249
+ const teamRole = getTeamRole(teamId, user)
250
+
251
+ // Only owner (or system admin) can modify team info
252
+ if (!user.isAdmin && teamRole !== 'owner') {
253
+ return c.json({ error: 'Only team owner can modify team info' }, 403)
254
+ }
255
+
256
+ let body: Record<string, unknown>
257
+ try {
258
+ body = await c.req.json()
259
+ } catch {
260
+ return c.json({ error: 'Invalid JSON' }, 400)
261
+ }
262
+
263
+ const { name, slug, description } = body as {
264
+ name?: string
265
+ slug?: string
266
+ description?: string
267
+ }
268
+
269
+ const db = getDb()
270
+
271
+ // If changing slug, validate and check uniqueness
272
+ if (slug !== undefined) {
273
+ if (!isValidSlug(slug)) {
274
+ return c.json({ error: 'Invalid slug format' }, 400)
275
+ }
276
+ const existingSlug = db
277
+ .query(
278
+ 'SELECT id FROM teams WHERE slug = $slug AND id != $id AND deleted_at IS NULL',
279
+ )
280
+ .get({ $slug: slug, $id: teamId })
281
+ if (existingSlug) {
282
+ return c.json({ error: 'Slug already taken' }, 409)
283
+ }
284
+ }
285
+
286
+ const now = new Date().toISOString()
287
+ const updates: string[] = ['updated_at = $now']
288
+ const params: Record<string, string | null> = {
289
+ $id: teamId,
290
+ $now: now,
291
+ }
292
+
293
+ if (name !== undefined) {
294
+ updates.push('name = $name')
295
+ params.$name = name
296
+ }
297
+ if (slug !== undefined) {
298
+ updates.push('slug = $slug')
299
+ params.$slug = slug
300
+ }
301
+ if (description !== undefined) {
302
+ updates.push('description = $desc')
303
+ params.$desc = description
304
+ }
305
+
306
+ db.query(`UPDATE teams SET ${updates.join(', ')} WHERE id = $id`).run(params)
307
+
308
+ const updated = db
309
+ .query('SELECT * FROM teams WHERE id = $id')
310
+ .get({ $id: teamId })
311
+ return c.json(updated, 200)
312
+ })
313
+
314
+ // ---------------------------------------------------------------------------
315
+ // DELETE /teams/:id — Soft delete team
316
+ // ---------------------------------------------------------------------------
317
+
318
+ app.delete('/teams/:id', withAuth, async c => {
319
+ const teamId = c.req.param('id')!
320
+ const user = getUserInfo(c)
321
+ const result = getTeamOr404(c, teamId)
322
+ if ('error' in result) return result.error
323
+
324
+ const teamRole = getTeamRole(teamId, user)
325
+
326
+ // Only owner (or system admin) can delete
327
+ if (!user.isAdmin && teamRole !== 'owner') {
328
+ return c.json({ error: 'Only team owner can delete team' }, 403)
329
+ }
330
+
331
+ const db = getDb()
332
+
333
+ // Check for un-transferred environments
334
+ const envCount = db
335
+ .query(
336
+ 'SELECT COUNT(*) as count FROM team_environments WHERE team_id = $teamId',
337
+ )
338
+ .get({ $teamId: teamId }) as { count: number }
339
+ if (envCount.count > 0) {
340
+ return c.json(
341
+ {
342
+ error: 'Team has environments that must be transferred first',
343
+ },
344
+ 400,
345
+ )
346
+ }
347
+
348
+ const now = new Date().toISOString()
349
+ db.query('UPDATE teams SET deleted_at = $now WHERE id = $id').run({
350
+ $now: now,
351
+ $id: teamId,
352
+ })
353
+
354
+ // Remove team ownership from sessions (orphan them)
355
+ db.query(
356
+ "DELETE FROM session_owners WHERE owner_type = 'team' AND owner_id = $teamId",
357
+ ).run({ $teamId: teamId })
358
+
359
+ return c.json({ ok: true }, 200)
360
+ })
361
+
362
+ // ---------------------------------------------------------------------------
363
+ // GET /teams/:id/members — List members
364
+ // ---------------------------------------------------------------------------
365
+
366
+ app.get('/teams/:id/members', withAuth, async c => {
367
+ const teamId = c.req.param('id')!
368
+ const user = getUserInfo(c)
369
+ const result = getTeamOr404(c, teamId)
370
+ if ('error' in result) return result.error
371
+
372
+ const teamRole = getTeamRole(teamId, user)
373
+ if (!teamRole) {
374
+ return c.json({ error: 'Not a team member' }, 403)
375
+ }
376
+
377
+ const db = getDb()
378
+ const members = db
379
+ .query(
380
+ `SELECT tm.*, u.username FROM team_members tm
381
+ LEFT JOIN users u ON u.id = tm.user_id
382
+ WHERE tm.team_id = $teamId`,
383
+ )
384
+ .all({ $teamId: teamId })
385
+
386
+ return c.json(members, 200)
387
+ })
388
+
389
+ // ---------------------------------------------------------------------------
390
+ // POST /teams/:id/members — Add member
391
+ // ---------------------------------------------------------------------------
392
+
393
+ app.post('/teams/:id/members', withAuth, async c => {
394
+ const teamId = c.req.param('id')!
395
+ const user = getUserInfo(c)
396
+ const result = getTeamOr404(c, teamId)
397
+ if ('error' in result) return result.error
398
+
399
+ const teamRole = getTeamRole(teamId, user)
400
+
401
+ // Only owner/admin (or system admin) can add members
402
+ if (!user.isAdmin && teamRole !== 'owner' && teamRole !== 'admin') {
403
+ return c.json({ error: 'Only team owner/admin can add members' }, 403)
404
+ }
405
+
406
+ let body: Record<string, unknown>
407
+ try {
408
+ body = await c.req.json()
409
+ } catch {
410
+ return c.json({ error: 'Invalid JSON' }, 400)
411
+ }
412
+
413
+ const { userId, role } = body as {
414
+ userId?: string
415
+ role?: string
416
+ }
417
+
418
+ if (!userId) {
419
+ return c.json({ error: 'userId is required' }, 400)
420
+ }
421
+
422
+ const validRoles = ['owner', 'admin', 'member']
423
+ const memberRole = role || 'member'
424
+ if (!validRoles.includes(memberRole)) {
425
+ return c.json({ error: 'Invalid role' }, 400)
426
+ }
427
+
428
+ const db = getDb()
429
+
430
+ // Check user exists
431
+ const targetUser = db
432
+ .query('SELECT id FROM users WHERE id = $id')
433
+ .get({ $id: userId })
434
+ if (!targetUser) {
435
+ return c.json({ error: 'User not found' }, 404)
436
+ }
437
+
438
+ // Check not already a member
439
+ const existing = db
440
+ .query(
441
+ 'SELECT user_id FROM team_members WHERE team_id = $teamId AND user_id = $userId',
442
+ )
443
+ .get({ $teamId: teamId, $userId: userId })
444
+ if (existing) {
445
+ return c.json({ error: 'User is already a member' }, 409)
446
+ }
447
+
448
+ const now = new Date().toISOString()
449
+ const addedBy = user.isAdmin
450
+ ? (result.team.created_by as string)
451
+ : user.userId
452
+
453
+ db.query(
454
+ `INSERT INTO team_members (team_id, user_id, role, added_at, added_by)
455
+ VALUES ($teamId, $userId, $role, $now, $addedBy)`,
456
+ ).run({
457
+ $teamId: teamId,
458
+ $userId: userId,
459
+ $role: memberRole,
460
+ $now: now,
461
+ $addedBy: addedBy,
462
+ })
463
+
464
+ return c.json({ ok: true, role: memberRole }, 201)
465
+ })
466
+
467
+ // ---------------------------------------------------------------------------
468
+ // PATCH /teams/:id/members/:userId — Change member role
469
+ // ---------------------------------------------------------------------------
470
+
471
+ app.patch('/teams/:id/members/:userId', withAuth, async c => {
472
+ const teamId = c.req.param('id')!
473
+ const targetUserId = c.req.param('userId')!
474
+ const user = getUserInfo(c)
475
+ const result = getTeamOr404(c, teamId)
476
+ if ('error' in result) return result.error
477
+
478
+ const teamRole = getTeamRole(teamId, user)
479
+
480
+ // Must be at least admin to change roles
481
+ if (!user.isAdmin && teamRole !== 'owner' && teamRole !== 'admin') {
482
+ return c.json({ error: 'Insufficient permissions' }, 403)
483
+ }
484
+
485
+ const db = getDb()
486
+
487
+ // Team admin cannot change owner/admin roles
488
+ if (!user.isAdmin && teamRole === 'admin') {
489
+ const targetMember = db
490
+ .query(
491
+ 'SELECT role FROM team_members WHERE team_id = $teamId AND user_id = $userId',
492
+ )
493
+ .get({ $teamId: teamId, $userId: targetUserId }) as {
494
+ role: string
495
+ } | null
496
+ if (
497
+ targetMember &&
498
+ (targetMember.role === 'owner' || targetMember.role === 'admin')
499
+ ) {
500
+ return c.json(
501
+ { error: 'Team admin cannot change owner/admin roles' },
502
+ 403,
503
+ )
504
+ }
505
+ }
506
+
507
+ let body: Record<string, unknown>
508
+ try {
509
+ body = await c.req.json()
510
+ } catch {
511
+ return c.json({ error: 'Invalid JSON' }, 400)
512
+ }
513
+
514
+ const { role } = body as { role?: string }
515
+ if (!role) {
516
+ return c.json({ error: 'role is required' }, 400)
517
+ }
518
+
519
+ const validRoles = ['owner', 'admin', 'member']
520
+ if (!validRoles.includes(role)) {
521
+ return c.json({ error: 'Invalid role' }, 400)
522
+ }
523
+
524
+ // Check target is actually a member
525
+ const currentMember = db
526
+ .query(
527
+ 'SELECT role FROM team_members WHERE team_id = $teamId AND user_id = $userId',
528
+ )
529
+ .get({ $teamId: teamId, $userId: targetUserId }) as {
530
+ role: string
531
+ } | null
532
+ if (!currentMember) {
533
+ return c.json({ error: 'User is not a member' }, 404)
534
+ }
535
+
536
+ // If changing from owner to something else, check there are other owners
537
+ if (currentMember.role === 'owner' && role !== 'owner') {
538
+ const ownerCount = db
539
+ .query(
540
+ `SELECT COUNT(*) as count FROM team_members
541
+ WHERE team_id = $teamId AND role = 'owner'`,
542
+ )
543
+ .get({ $teamId: teamId }) as { count: number }
544
+ if (ownerCount.count <= 1) {
545
+ return c.json({ error: 'Cannot demote the last owner' }, 400)
546
+ }
547
+ }
548
+
549
+ db.query(
550
+ `UPDATE team_members SET role = $role WHERE team_id = $teamId AND user_id = $userId`,
551
+ ).run({ $role: role, $teamId: teamId, $userId: targetUserId })
552
+
553
+ return c.json({ ok: true, role }, 200)
554
+ })
555
+
556
+ // ---------------------------------------------------------------------------
557
+ // DELETE /teams/:id/members/:userId — Remove member
558
+ // ---------------------------------------------------------------------------
559
+
560
+ app.delete('/teams/:id/members/:userId', withAuth, async c => {
561
+ const teamId = c.req.param('id')!
562
+ const targetUserId = c.req.param('userId')!
563
+ const user = getUserInfo(c)
564
+ const result = getTeamOr404(c, teamId)
565
+ if ('error' in result) return result.error
566
+
567
+ const teamRole = getTeamRole(teamId, user)
568
+ const db = getDb()
569
+
570
+ // Check target is a member
571
+ const targetMember = db
572
+ .query(
573
+ 'SELECT role FROM team_members WHERE team_id = $teamId AND user_id = $userId',
574
+ )
575
+ .get({ $teamId: teamId, $userId: targetUserId }) as {
576
+ role: string
577
+ } | null
578
+ if (!targetMember) {
579
+ return c.json({ error: 'User is not a member' }, 404)
580
+ }
581
+
582
+ // Members can only remove themselves
583
+ if (!user.isAdmin && teamRole === 'member') {
584
+ if (targetUserId !== user.userId) {
585
+ return c.json({ error: 'Members can only remove themselves' }, 403)
586
+ }
587
+ }
588
+
589
+ // Owner/admin can remove anyone (except constraints below)
590
+ if (
591
+ !user.isAdmin &&
592
+ teamRole !== 'owner' &&
593
+ teamRole !== 'admin' &&
594
+ targetUserId !== user.userId
595
+ ) {
596
+ return c.json({ error: 'Insufficient permissions' }, 403)
597
+ }
598
+
599
+ // Cannot remove the last owner
600
+ if (targetMember.role === 'owner') {
601
+ const ownerCount = db
602
+ .query(
603
+ `SELECT COUNT(*) as count FROM team_members
604
+ WHERE team_id = $teamId AND role = 'owner'`,
605
+ )
606
+ .get({ $teamId: teamId }) as { count: number }
607
+ if (ownerCount.count <= 1) {
608
+ return c.json({ error: 'Cannot remove the last owner' }, 400)
609
+ }
610
+ }
611
+
612
+ db.query(
613
+ 'DELETE FROM team_members WHERE team_id = $teamId AND user_id = $userId',
614
+ ).run({ $teamId: teamId, $userId: targetUserId })
615
+
616
+ return c.json({ ok: true }, 200)
617
+ })
618
+
619
+ // ---------------------------------------------------------------------------
620
+ // POST /teams/:id/invitations — Create invitation
621
+ // ---------------------------------------------------------------------------
622
+
623
+ app.post('/teams/:id/invitations', withAuth, async c => {
624
+ const teamId = c.req.param('id')!
625
+ const user = getUserInfo(c)
626
+ const result = getTeamOr404(c, teamId)
627
+ if ('error' in result) return result.error
628
+
629
+ const teamRole = getTeamRole(teamId, user)
630
+
631
+ // Only owner/admin can create invitations
632
+ if (!user.isAdmin && teamRole !== 'owner' && teamRole !== 'admin') {
633
+ return c.json(
634
+ { error: 'Only team owner/admin can create invitations' },
635
+ 403,
636
+ )
637
+ }
638
+
639
+ let body: Record<string, unknown>
640
+ try {
641
+ body = await c.req.json()
642
+ } catch {
643
+ return c.json({ error: 'Invalid JSON' }, 400)
644
+ }
645
+
646
+ const { role, expiresIn, maxUses } = body as {
647
+ role?: string
648
+ expiresIn?: number
649
+ maxUses?: number
650
+ }
651
+
652
+ const inviteRole = role || 'member'
653
+ const validRoles = ['admin', 'member']
654
+ if (!validRoles.includes(inviteRole)) {
655
+ return c.json({ error: 'Invalid role' }, 400)
656
+ }
657
+
658
+ const token = `inv_${randomUUID().replace(/-/g, '')}`
659
+ const tokenHash = createHash('sha256').update(token).digest('hex')
660
+ const now = new Date()
661
+ const expiresAt = new Date(
662
+ now.getTime() + (expiresIn || 86400) * 1000,
663
+ ).toISOString()
664
+ const maxUsesValue = maxUses || 1
665
+
666
+ const db = getDb()
667
+ const createdBy = user.isAdmin
668
+ ? (result.team.created_by as string)
669
+ : user.userId
670
+
671
+ db.query(
672
+ `INSERT INTO invitations (token_hash, team_id, role, expires_at, max_uses, uses, created_by)
673
+ VALUES ($hash, $teamId, $role, $exp, $maxUses, 0, $by)`,
674
+ ).run({
675
+ $hash: tokenHash,
676
+ $teamId: teamId,
677
+ $role: inviteRole,
678
+ $exp: expiresAt,
679
+ $maxUses: maxUsesValue,
680
+ $by: createdBy,
681
+ })
682
+
683
+ return c.json(
684
+ {
685
+ token,
686
+ inviteToken: token,
687
+ role: inviteRole,
688
+ expiresAt,
689
+ maxUses: maxUsesValue,
690
+ teamId,
691
+ },
692
+ 201,
693
+ )
694
+ })
695
+
696
+ // ---------------------------------------------------------------------------
697
+ // DELETE /teams/:id/invitations/:token — Revoke invitation
698
+ // ---------------------------------------------------------------------------
699
+
700
+ app.delete('/teams/:id/invitations/:token', withAuth, async c => {
701
+ const teamId = c.req.param('id')!
702
+ const token = c.req.param('token')!
703
+ const user = getUserInfo(c)
704
+ const result = getTeamOr404(c, teamId)
705
+ if ('error' in result) return result.error
706
+
707
+ const teamRole = getTeamRole(teamId, user)
708
+
709
+ // Only owner/admin can revoke invitations
710
+ if (!user.isAdmin && teamRole !== 'owner' && teamRole !== 'admin') {
711
+ return c.json(
712
+ { error: 'Only team owner/admin can revoke invitations' },
713
+ 403,
714
+ )
715
+ }
716
+
717
+ const db = getDb()
718
+ const tokenHash = createHash('sha256').update(token).digest('hex')
719
+
720
+ // Verify invitation belongs to this team
721
+ const invitation = db
722
+ .query(
723
+ 'SELECT * FROM invitations WHERE token_hash = $hash AND team_id = $teamId',
724
+ )
725
+ .get({ $hash: tokenHash, $teamId: teamId })
726
+ if (!invitation) {
727
+ return c.json({ error: 'Invitation not found' }, 404)
728
+ }
729
+
730
+ // Delete the invitation (revoke)
731
+ db.query('DELETE FROM invitations WHERE token_hash = $hash').run({
732
+ $hash: tokenHash,
733
+ })
734
+
735
+ return c.json({ ok: true }, 200)
736
+ })
737
+
738
+ // ---------------------------------------------------------------------------
739
+ // GET /teams/:id/environments — List team environments
740
+ // ---------------------------------------------------------------------------
741
+
742
+ app.get('/teams/:id/environments', withAuth, async c => {
743
+ const teamId = c.req.param('id')!
744
+ const user = getUserInfo(c)
745
+ const result = getTeamOr404(c, teamId)
746
+ if ('error' in result) return result.error
747
+
748
+ const teamRole = getTeamRole(teamId, user)
749
+ if (!teamRole) {
750
+ return c.json({ error: 'Not a team member' }, 403)
751
+ }
752
+
753
+ const db = getDb()
754
+ const envs = db
755
+ .query(
756
+ `SELECT te.environment_id as id, te.environment_id
757
+ FROM team_environments te
758
+ WHERE te.team_id = $teamId`,
759
+ )
760
+ .all({ $teamId: teamId })
761
+
762
+ return c.json(envs, 200)
763
+ })
764
+
765
+ // ---------------------------------------------------------------------------
766
+ // POST /environments/:id/transfer — Transfer environment to another team
767
+ // ---------------------------------------------------------------------------
768
+
769
+ app.post('/environments/:id/transfer', withAuth, async c => {
770
+ const envId = c.req.param('id')!
771
+ const user = getUserInfo(c)
772
+
773
+ let body: Record<string, unknown>
774
+ try {
775
+ body = await c.req.json()
776
+ } catch {
777
+ return c.json({ error: 'Invalid JSON' }, 400)
778
+ }
779
+
780
+ const { toTeamId } = body as { toTeamId?: string }
781
+ if (!toTeamId) {
782
+ return c.json({ error: 'toTeamId is required' }, 400)
783
+ }
784
+
785
+ const db = getDb()
786
+
787
+ // Check target team exists
788
+ const targetTeam = db
789
+ .query('SELECT * FROM teams WHERE id = $id AND deleted_at IS NULL')
790
+ .get({ $id: toTeamId })
791
+ if (!targetTeam) {
792
+ return c.json({ error: 'Target team not found' }, 404)
793
+ }
794
+
795
+ // Find source team
796
+ const sourceRow = db
797
+ .query(
798
+ 'SELECT team_id FROM team_environments WHERE environment_id = $envId',
799
+ )
800
+ .get({ $envId: envId }) as { team_id: string } | null
801
+
802
+ if (sourceRow) {
803
+ const teamRole = getTeamRole(sourceRow.team_id, user)
804
+ // Only owner/admin (or system admin) can transfer
805
+ if (!user.isAdmin && teamRole !== 'owner' && teamRole !== 'admin') {
806
+ return c.json(
807
+ {
808
+ error: 'Only team owner/admin can transfer environments',
809
+ },
810
+ 403,
811
+ )
812
+ }
813
+ } else if (!user.isAdmin) {
814
+ // Environment not in any team — only admin can transfer
815
+ return c.json({ error: 'Environment not found in any team' }, 404)
816
+ }
817
+
818
+ // Remove from source team
819
+ db.query('DELETE FROM team_environments WHERE environment_id = $envId').run({
820
+ $envId: envId,
821
+ })
822
+
823
+ // Add to target team
824
+ db.query(
825
+ `INSERT OR REPLACE INTO team_environments (team_id, environment_id)
826
+ VALUES ($teamId, $envId)`,
827
+ ).run({ $teamId: toTeamId, $envId: envId })
828
+
829
+ // Transfer session ownership for sessions in this environment
830
+ // Update session_owners: remove old team owners, add new team owner
831
+ if (sourceRow) {
832
+ db.query(
833
+ `DELETE FROM session_owners
834
+ WHERE owner_type = 'team' AND owner_id = $oldTeamId
835
+ AND session_id IN (
836
+ SELECT id FROM sessions WHERE environment_id = $envId
837
+ )`,
838
+ ).run({ $oldTeamId: sourceRow.team_id, $envId: envId })
839
+ }
840
+
841
+ // Add new team as owner for sessions in this environment
842
+ db.query(
843
+ `INSERT OR IGNORE INTO session_owners (session_id, owner_type, owner_id)
844
+ SELECT id, 'team', $teamId FROM sessions WHERE environment_id = $envId`,
845
+ ).run({ $teamId: toTeamId, $envId: envId })
846
+
847
+ return c.json({ ok: true }, 200)
848
+ })
849
+
850
+ export default app