@saluzi/saluzi-edu 0.2.65 → 0.2.67

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 (178) hide show
  1. package/dist/cli.js +534 -349
  2. package/dist/guide/guide-data.json +8 -0
  3. package/dist/rcs-schema.sql +23 -0
  4. package/dist/rcs-web/assets/{AdminPanel-CqP_94xa.js → AdminPanel-94uchXH7.js} +1 -1
  5. package/dist/rcs-web/assets/ArtifactView-CT0Fu4U5.js +6 -0
  6. package/dist/rcs-web/assets/ArtifactsGallery-DgK1CSno.js +339 -0
  7. package/{packages/remote-control-server/web/dist/assets/Claim-BvfARQ6Q.js → dist/rcs-web/assets/Claim-C7V1Q01Y.js} +1 -1
  8. package/dist/rcs-web/assets/{Dashboard-BDGi4XpN.js → Dashboard-NLM7Kgm8.js} +2 -2
  9. package/{packages/remote-control-server/web/dist/assets/Join-PPDYBhsJ.js → dist/rcs-web/assets/Join-BhjsvbAP.js} +1 -1
  10. package/dist/rcs-web/assets/{Login-JprQIEpr.js → Login-DRFE4F-F.js} +1 -1
  11. package/dist/rcs-web/assets/{Register-cuGuAE83.js → Register-FgLOFB4h.js} +1 -1
  12. package/dist/rcs-web/assets/SessionDetail-3P77L-sJ.js +60 -0
  13. package/dist/rcs-web/assets/SessionVisibilityBadge-raEV1sW8.js +11 -0
  14. package/dist/rcs-web/assets/{Setup-Buwa9Y3M.js → Setup-DDJHMu0z.js} +1 -1
  15. package/dist/rcs-web/assets/ShareLanding-DG5JbaWg.js +1 -0
  16. package/dist/rcs-web/assets/{TeamCreate-CaoinTSG.js → TeamCreate-D_AnatYF.js} +1 -1
  17. package/dist/rcs-web/assets/{TeamDetail-D5ahen24.js → TeamDetail-C1FFiAj2.js} +2 -2
  18. package/dist/rcs-web/assets/{TeamList-qCuKKSH3.js → TeamList-tfsnwvZG.js} +1 -1
  19. package/dist/rcs-web/assets/{TeamSettings-BaIwImrp.js → TeamSettings-FeTuY9_I.js} +1 -1
  20. package/dist/rcs-web/assets/{UserSearchInput-BxcYR_HU.js → UserSearchInput-Bu3RXDYh.js} +1 -1
  21. package/dist/rcs-web/assets/{UserSettings-zfGfcuWs.js → UserSettings-BrjmELAZ.js} +1 -1
  22. package/dist/rcs-web/assets/{arc-DwAbF6eM.js → arc-Di9w8tGK.js} +1 -1
  23. package/dist/rcs-web/assets/{architectureDiagram-3BPJPVTR-uMOwOwiN.js → architectureDiagram-3BPJPVTR-FT-IsfVA.js} +1 -1
  24. package/{packages/remote-control-server/web/dist/assets/blockDiagram-GPEHLZMM-B5BqFTkP.js → dist/rcs-web/assets/blockDiagram-GPEHLZMM-DryBvbP3.js} +1 -1
  25. package/dist/rcs-web/assets/{c4Diagram-AAUBKEIU-D6eienz2.js → c4Diagram-AAUBKEIU-HWAmxH56.js} +1 -1
  26. package/dist/rcs-web/assets/channel-N4W03sy5.js +1 -0
  27. package/dist/rcs-web/assets/{chunk-2J33WTMH-DqVxv8yW.js → chunk-2J33WTMH-AdjHpC7B.js} +1 -1
  28. package/dist/rcs-web/assets/{chunk-4BX2VUAB-X5o4x8om.js → chunk-4BX2VUAB-CpZ-59LR.js} +1 -1
  29. package/dist/rcs-web/assets/{chunk-55IACEB6-D7G25_Y5.js → chunk-55IACEB6-BBSmURaz.js} +1 -1
  30. package/{packages/remote-control-server/web/dist/assets/chunk-727SXJPM-CoXI1FIy.js → dist/rcs-web/assets/chunk-727SXJPM-1k6tgn8s.js} +1 -1
  31. package/dist/rcs-web/assets/{chunk-AQP2D5EJ-C73Crhqe.js → chunk-AQP2D5EJ-im0ij4Kj.js} +1 -1
  32. package/dist/rcs-web/assets/{chunk-FMBD7UC4-DVzxIsSE.js → chunk-FMBD7UC4-Cs3z8_hw.js} +1 -1
  33. package/dist/rcs-web/assets/{chunk-ND2GUHAM-C6_oEhLf.js → chunk-ND2GUHAM-BA9iE2wc.js} +1 -1
  34. package/dist/rcs-web/assets/{chunk-QZHKN3VN-COvH7k5r.js → chunk-QZHKN3VN-DbvzoKsw.js} +1 -1
  35. package/dist/rcs-web/assets/classDiagram-4FO5ZUOK-CcBgewcu.js +1 -0
  36. package/dist/rcs-web/assets/classDiagram-v2-Q7XG4LA2-CcBgewcu.js +1 -0
  37. package/dist/rcs-web/assets/{code-block-IT6T5CEO-CE-2f_-K.js → code-block-IT6T5CEO-wgmiR0vF.js} +1 -1
  38. package/dist/rcs-web/assets/{copy-9pzb1Po6.js → copy-BD6qrRfd.js} +1 -1
  39. package/dist/rcs-web/assets/{cose-bilkent-S5V4N54A-B9sMDFRe.js → cose-bilkent-S5V4N54A-DT4C5Yz2.js} +1 -1
  40. package/dist/rcs-web/assets/{dagre-BM42HDAG-DB1jXylM.js → dagre-BM42HDAG-CRf1H6Hv.js} +1 -1
  41. package/dist/rcs-web/assets/{diagram-2AECGRRQ-MNuqSRmg.js → diagram-2AECGRRQ-BChRpS-u.js} +1 -1
  42. package/dist/rcs-web/assets/{diagram-5GNKFQAL-BZwhvaEY.js → diagram-5GNKFQAL-BicbIYus.js} +1 -1
  43. package/dist/rcs-web/assets/{diagram-KO2AKTUF-3Wp2PnI2.js → diagram-KO2AKTUF-DCfQNjGq.js} +1 -1
  44. package/dist/rcs-web/assets/{diagram-LMA3HP47-CTcV1SBj.js → diagram-LMA3HP47-mMyd4aUd.js} +1 -1
  45. package/dist/rcs-web/assets/{diagram-OG6HWLK6-DzXhSQDC.js → diagram-OG6HWLK6-DUWhyWTm.js} +1 -1
  46. package/dist/rcs-web/assets/dialog-DympH6_o.js +1 -0
  47. package/dist/rcs-web/assets/{erDiagram-TEJ5UH35-CwSihkB8.js → erDiagram-TEJ5UH35-DnBH3GMK.js} +1 -1
  48. package/dist/rcs-web/assets/external-link-J-x6ty1h.js +6 -0
  49. package/dist/rcs-web/assets/{flowDiagram-I6XJVG4X-BSuXctT4.js → flowDiagram-I6XJVG4X-Ba7lo6ds.js} +1 -1
  50. package/{packages/remote-control-server/web/dist/assets/ganttDiagram-6RSMTGT7-B87oxjmY.js → dist/rcs-web/assets/ganttDiagram-6RSMTGT7-CKkj67eu.js} +1 -1
  51. package/dist/rcs-web/assets/{gitGraphDiagram-PVQCEYII-BRrwOi3C.js → gitGraphDiagram-PVQCEYII-205YdSRe.js} +1 -1
  52. package/dist/rcs-web/assets/{infoDiagram-5YYISTIA-B-8QXS0n.js → infoDiagram-5YYISTIA-CpPvGigJ.js} +1 -1
  53. package/dist/rcs-web/assets/{ishikawaDiagram-YF4QCWOH-Bau9MU6C.js → ishikawaDiagram-YF4QCWOH-D0ZEWQy7.js} +1 -1
  54. package/dist/rcs-web/assets/{journeyDiagram-JHISSGLW-D_tAp--R.js → journeyDiagram-JHISSGLW-DVl0M9a7.js} +1 -1
  55. package/dist/rcs-web/assets/{kanban-definition-UN3LZRKU---UIvjMZ.js → kanban-definition-UN3LZRKU-Dkr8uJC-.js} +1 -1
  56. package/dist/rcs-web/assets/{linear-CIDLjd5h.js → linear-CIdvseqY.js} +1 -1
  57. package/dist/rcs-web/assets/main-B8hkVvfg.js +265 -0
  58. package/dist/rcs-web/assets/main-C2O-g177.css +1 -0
  59. package/dist/rcs-web/assets/{mermaid-VLURNSYL-Cu5A5sMk.js → mermaid-VLURNSYL-DdTSeoZS.js} +4 -4
  60. package/dist/rcs-web/assets/{mermaid.core-CEJOtCGw.js → mermaid.core-CtvzCSq2.js} +5 -5
  61. package/{packages/remote-control-server/web/dist/assets/mindmap-definition-RKZ34NQL-ldyccyCP.js → dist/rcs-web/assets/mindmap-definition-RKZ34NQL-CMlDCaV_.js} +1 -1
  62. package/dist/rcs-web/assets/{pieDiagram-4H26LBE5-CmghofeF.js → pieDiagram-4H26LBE5-C6xMizTL.js} +1 -1
  63. package/{packages/remote-control-server/web/dist/assets/quadrantDiagram-W4KKPZXB-B-2N2UHr.js → dist/rcs-web/assets/quadrantDiagram-W4KKPZXB-D6epraE1.js} +1 -1
  64. package/dist/rcs-web/assets/{requirementDiagram-4Y6WPE33-39jlO9Y8.js → requirementDiagram-4Y6WPE33-BHmML6bk.js} +1 -1
  65. package/{packages/remote-control-server/web/dist/assets/sankeyDiagram-5OEKKPKP-DdY7KNFB.js → dist/rcs-web/assets/sankeyDiagram-5OEKKPKP-CIKij54Y.js} +1 -1
  66. package/dist/rcs-web/assets/{sequenceDiagram-3UESZ5HK-D2k5rX6x.js → sequenceDiagram-3UESZ5HK-CY0y5zdR.js} +1 -1
  67. package/dist/rcs-web/assets/{stateDiagram-AJRCARHV-CP9N457o.js → stateDiagram-AJRCARHV-FBhNz1Lc.js} +1 -1
  68. package/dist/rcs-web/assets/{stateDiagram-v2-BHNVJYJU-CPhZYlL0.js → stateDiagram-v2-BHNVJYJU-m7ruBTu5.js} +1 -1
  69. package/{packages/remote-control-server/web/dist/assets/timeline-definition-PNZ67QCA-DSEPBO8O.js → dist/rcs-web/assets/timeline-definition-PNZ67QCA-CJopHs1x.js} +1 -1
  70. package/dist/rcs-web/assets/{vennDiagram-CIIHVFJN-CkNMwCJ3.js → vennDiagram-CIIHVFJN-DF_uHOP7.js} +1 -1
  71. package/dist/rcs-web/assets/{wardleyDiagram-YWT4CUSO-Cxd-F60s.js → wardleyDiagram-YWT4CUSO-DQmjIZJA.js} +1 -1
  72. package/{packages/remote-control-server/web/dist/assets/xychartDiagram-2RQKCTM6-BLL105NN.js → dist/rcs-web/assets/xychartDiagram-2RQKCTM6-v8zL-j4G.js} +1 -1
  73. package/dist/rcs-web/index.html +2 -2
  74. package/package.json +1 -1
  75. package/packages/remote-control-server/src/__tests__/artifacts.test.ts +370 -0
  76. package/packages/remote-control-server/src/__tests__/attach-wait.test.ts +54 -0
  77. package/packages/remote-control-server/src/__tests__/attach.test.ts +37 -0
  78. package/packages/remote-control-server/src/__tests__/helpers/mock-config.ts +1 -0
  79. package/packages/remote-control-server/src/__tests__/low-priority-fixes.test.ts +95 -0
  80. package/packages/remote-control-server/src/__tests__/routes.test.ts +69 -0
  81. package/packages/remote-control-server/src/config.ts +6 -0
  82. package/packages/remote-control-server/src/db/rcs-schema.sql +23 -0
  83. package/packages/remote-control-server/src/index.ts +9 -0
  84. package/packages/remote-control-server/src/registerRoutes.ts +8 -0
  85. package/packages/remote-control-server/src/routes/v1/artifacts.ts +179 -0
  86. package/packages/remote-control-server/src/routes/v1/environments.ts +17 -9
  87. package/packages/remote-control-server/src/routes/v1/environments.work.ts +2 -2
  88. package/packages/remote-control-server/src/routes/v1/sessions.ts +39 -9
  89. package/packages/remote-control-server/src/routes/web/artifacts.ts +249 -0
  90. package/packages/remote-control-server/src/routes/web/sessions.ts +25 -22
  91. package/packages/remote-control-server/src/services/artifacts.ts +378 -0
  92. package/packages/remote-control-server/src/services/environment.ts +25 -0
  93. package/packages/remote-control-server/web/dist/assets/{AdminPanel-CqP_94xa.js → AdminPanel-94uchXH7.js} +1 -1
  94. package/packages/remote-control-server/web/dist/assets/ArtifactView-CT0Fu4U5.js +6 -0
  95. package/packages/remote-control-server/web/dist/assets/ArtifactsGallery-DgK1CSno.js +339 -0
  96. package/{dist/rcs-web/assets/Claim-BvfARQ6Q.js → packages/remote-control-server/web/dist/assets/Claim-C7V1Q01Y.js} +1 -1
  97. package/packages/remote-control-server/web/dist/assets/{Dashboard-BDGi4XpN.js → Dashboard-NLM7Kgm8.js} +2 -2
  98. package/{dist/rcs-web/assets/Join-PPDYBhsJ.js → packages/remote-control-server/web/dist/assets/Join-BhjsvbAP.js} +1 -1
  99. package/packages/remote-control-server/web/dist/assets/{Login-JprQIEpr.js → Login-DRFE4F-F.js} +1 -1
  100. package/packages/remote-control-server/web/dist/assets/{Register-cuGuAE83.js → Register-FgLOFB4h.js} +1 -1
  101. package/packages/remote-control-server/web/dist/assets/SessionDetail-3P77L-sJ.js +60 -0
  102. package/packages/remote-control-server/web/dist/assets/SessionVisibilityBadge-raEV1sW8.js +11 -0
  103. package/packages/remote-control-server/web/dist/assets/{Setup-Buwa9Y3M.js → Setup-DDJHMu0z.js} +1 -1
  104. package/packages/remote-control-server/web/dist/assets/ShareLanding-DG5JbaWg.js +1 -0
  105. package/packages/remote-control-server/web/dist/assets/{TeamCreate-CaoinTSG.js → TeamCreate-D_AnatYF.js} +1 -1
  106. package/packages/remote-control-server/web/dist/assets/{TeamDetail-D5ahen24.js → TeamDetail-C1FFiAj2.js} +2 -2
  107. package/packages/remote-control-server/web/dist/assets/{TeamList-qCuKKSH3.js → TeamList-tfsnwvZG.js} +1 -1
  108. package/packages/remote-control-server/web/dist/assets/{TeamSettings-BaIwImrp.js → TeamSettings-FeTuY9_I.js} +1 -1
  109. package/packages/remote-control-server/web/dist/assets/{UserSearchInput-BxcYR_HU.js → UserSearchInput-Bu3RXDYh.js} +1 -1
  110. package/packages/remote-control-server/web/dist/assets/{UserSettings-zfGfcuWs.js → UserSettings-BrjmELAZ.js} +1 -1
  111. package/packages/remote-control-server/web/dist/assets/{arc-DwAbF6eM.js → arc-Di9w8tGK.js} +1 -1
  112. package/packages/remote-control-server/web/dist/assets/{architectureDiagram-3BPJPVTR-uMOwOwiN.js → architectureDiagram-3BPJPVTR-FT-IsfVA.js} +1 -1
  113. package/{dist/rcs-web/assets/blockDiagram-GPEHLZMM-B5BqFTkP.js → packages/remote-control-server/web/dist/assets/blockDiagram-GPEHLZMM-DryBvbP3.js} +1 -1
  114. package/packages/remote-control-server/web/dist/assets/{c4Diagram-AAUBKEIU-D6eienz2.js → c4Diagram-AAUBKEIU-HWAmxH56.js} +1 -1
  115. package/packages/remote-control-server/web/dist/assets/channel-N4W03sy5.js +1 -0
  116. package/packages/remote-control-server/web/dist/assets/{chunk-2J33WTMH-DqVxv8yW.js → chunk-2J33WTMH-AdjHpC7B.js} +1 -1
  117. package/packages/remote-control-server/web/dist/assets/{chunk-4BX2VUAB-X5o4x8om.js → chunk-4BX2VUAB-CpZ-59LR.js} +1 -1
  118. package/packages/remote-control-server/web/dist/assets/{chunk-55IACEB6-D7G25_Y5.js → chunk-55IACEB6-BBSmURaz.js} +1 -1
  119. package/{dist/rcs-web/assets/chunk-727SXJPM-CoXI1FIy.js → packages/remote-control-server/web/dist/assets/chunk-727SXJPM-1k6tgn8s.js} +1 -1
  120. package/packages/remote-control-server/web/dist/assets/{chunk-AQP2D5EJ-C73Crhqe.js → chunk-AQP2D5EJ-im0ij4Kj.js} +1 -1
  121. package/packages/remote-control-server/web/dist/assets/{chunk-FMBD7UC4-DVzxIsSE.js → chunk-FMBD7UC4-Cs3z8_hw.js} +1 -1
  122. package/packages/remote-control-server/web/dist/assets/{chunk-ND2GUHAM-C6_oEhLf.js → chunk-ND2GUHAM-BA9iE2wc.js} +1 -1
  123. package/packages/remote-control-server/web/dist/assets/{chunk-QZHKN3VN-COvH7k5r.js → chunk-QZHKN3VN-DbvzoKsw.js} +1 -1
  124. package/packages/remote-control-server/web/dist/assets/classDiagram-4FO5ZUOK-CcBgewcu.js +1 -0
  125. package/packages/remote-control-server/web/dist/assets/classDiagram-v2-Q7XG4LA2-CcBgewcu.js +1 -0
  126. package/packages/remote-control-server/web/dist/assets/{code-block-IT6T5CEO-CE-2f_-K.js → code-block-IT6T5CEO-wgmiR0vF.js} +1 -1
  127. package/packages/remote-control-server/web/dist/assets/{copy-9pzb1Po6.js → copy-BD6qrRfd.js} +1 -1
  128. package/packages/remote-control-server/web/dist/assets/{cose-bilkent-S5V4N54A-B9sMDFRe.js → cose-bilkent-S5V4N54A-DT4C5Yz2.js} +1 -1
  129. package/packages/remote-control-server/web/dist/assets/{dagre-BM42HDAG-DB1jXylM.js → dagre-BM42HDAG-CRf1H6Hv.js} +1 -1
  130. package/packages/remote-control-server/web/dist/assets/{diagram-2AECGRRQ-MNuqSRmg.js → diagram-2AECGRRQ-BChRpS-u.js} +1 -1
  131. package/packages/remote-control-server/web/dist/assets/{diagram-5GNKFQAL-BZwhvaEY.js → diagram-5GNKFQAL-BicbIYus.js} +1 -1
  132. package/packages/remote-control-server/web/dist/assets/{diagram-KO2AKTUF-3Wp2PnI2.js → diagram-KO2AKTUF-DCfQNjGq.js} +1 -1
  133. package/packages/remote-control-server/web/dist/assets/{diagram-LMA3HP47-CTcV1SBj.js → diagram-LMA3HP47-mMyd4aUd.js} +1 -1
  134. package/packages/remote-control-server/web/dist/assets/{diagram-OG6HWLK6-DzXhSQDC.js → diagram-OG6HWLK6-DUWhyWTm.js} +1 -1
  135. package/packages/remote-control-server/web/dist/assets/dialog-DympH6_o.js +1 -0
  136. package/packages/remote-control-server/web/dist/assets/{erDiagram-TEJ5UH35-CwSihkB8.js → erDiagram-TEJ5UH35-DnBH3GMK.js} +1 -1
  137. package/packages/remote-control-server/web/dist/assets/external-link-J-x6ty1h.js +6 -0
  138. package/packages/remote-control-server/web/dist/assets/{flowDiagram-I6XJVG4X-BSuXctT4.js → flowDiagram-I6XJVG4X-Ba7lo6ds.js} +1 -1
  139. package/{dist/rcs-web/assets/ganttDiagram-6RSMTGT7-B87oxjmY.js → packages/remote-control-server/web/dist/assets/ganttDiagram-6RSMTGT7-CKkj67eu.js} +1 -1
  140. package/packages/remote-control-server/web/dist/assets/{gitGraphDiagram-PVQCEYII-BRrwOi3C.js → gitGraphDiagram-PVQCEYII-205YdSRe.js} +1 -1
  141. package/packages/remote-control-server/web/dist/assets/{infoDiagram-5YYISTIA-B-8QXS0n.js → infoDiagram-5YYISTIA-CpPvGigJ.js} +1 -1
  142. package/packages/remote-control-server/web/dist/assets/{ishikawaDiagram-YF4QCWOH-Bau9MU6C.js → ishikawaDiagram-YF4QCWOH-D0ZEWQy7.js} +1 -1
  143. package/packages/remote-control-server/web/dist/assets/{journeyDiagram-JHISSGLW-D_tAp--R.js → journeyDiagram-JHISSGLW-DVl0M9a7.js} +1 -1
  144. package/packages/remote-control-server/web/dist/assets/{kanban-definition-UN3LZRKU---UIvjMZ.js → kanban-definition-UN3LZRKU-Dkr8uJC-.js} +1 -1
  145. package/packages/remote-control-server/web/dist/assets/{linear-CIDLjd5h.js → linear-CIdvseqY.js} +1 -1
  146. package/packages/remote-control-server/web/dist/assets/main-B8hkVvfg.js +265 -0
  147. package/packages/remote-control-server/web/dist/assets/main-C2O-g177.css +1 -0
  148. package/packages/remote-control-server/web/dist/assets/{mermaid-VLURNSYL-Cu5A5sMk.js → mermaid-VLURNSYL-DdTSeoZS.js} +4 -4
  149. package/packages/remote-control-server/web/dist/assets/{mermaid.core-CEJOtCGw.js → mermaid.core-CtvzCSq2.js} +5 -5
  150. package/{dist/rcs-web/assets/mindmap-definition-RKZ34NQL-ldyccyCP.js → packages/remote-control-server/web/dist/assets/mindmap-definition-RKZ34NQL-CMlDCaV_.js} +1 -1
  151. package/packages/remote-control-server/web/dist/assets/{pieDiagram-4H26LBE5-CmghofeF.js → pieDiagram-4H26LBE5-C6xMizTL.js} +1 -1
  152. package/{dist/rcs-web/assets/quadrantDiagram-W4KKPZXB-B-2N2UHr.js → packages/remote-control-server/web/dist/assets/quadrantDiagram-W4KKPZXB-D6epraE1.js} +1 -1
  153. package/packages/remote-control-server/web/dist/assets/{requirementDiagram-4Y6WPE33-39jlO9Y8.js → requirementDiagram-4Y6WPE33-BHmML6bk.js} +1 -1
  154. package/{dist/rcs-web/assets/sankeyDiagram-5OEKKPKP-DdY7KNFB.js → packages/remote-control-server/web/dist/assets/sankeyDiagram-5OEKKPKP-CIKij54Y.js} +1 -1
  155. package/packages/remote-control-server/web/dist/assets/{sequenceDiagram-3UESZ5HK-D2k5rX6x.js → sequenceDiagram-3UESZ5HK-CY0y5zdR.js} +1 -1
  156. package/packages/remote-control-server/web/dist/assets/{stateDiagram-AJRCARHV-CP9N457o.js → stateDiagram-AJRCARHV-FBhNz1Lc.js} +1 -1
  157. package/packages/remote-control-server/web/dist/assets/{stateDiagram-v2-BHNVJYJU-CPhZYlL0.js → stateDiagram-v2-BHNVJYJU-m7ruBTu5.js} +1 -1
  158. package/{dist/rcs-web/assets/timeline-definition-PNZ67QCA-DSEPBO8O.js → packages/remote-control-server/web/dist/assets/timeline-definition-PNZ67QCA-CJopHs1x.js} +1 -1
  159. package/packages/remote-control-server/web/dist/assets/{vennDiagram-CIIHVFJN-CkNMwCJ3.js → vennDiagram-CIIHVFJN-DF_uHOP7.js} +1 -1
  160. package/packages/remote-control-server/web/dist/assets/{wardleyDiagram-YWT4CUSO-Cxd-F60s.js → wardleyDiagram-YWT4CUSO-DQmjIZJA.js} +1 -1
  161. package/{dist/rcs-web/assets/xychartDiagram-2RQKCTM6-BLL105NN.js → packages/remote-control-server/web/dist/assets/xychartDiagram-2RQKCTM6-v8zL-j4G.js} +1 -1
  162. package/packages/remote-control-server/web/dist/index.html +2 -2
  163. package/dist/rcs-web/assets/SessionDetail-D-xhkyLo.js +0 -65
  164. package/dist/rcs-web/assets/SessionVisibilityBadge-91WyR9K6.js +0 -11
  165. package/dist/rcs-web/assets/ShareLanding-DhwgJb6V.js +0 -1
  166. package/dist/rcs-web/assets/channel-CCfqpGSl.js +0 -1
  167. package/dist/rcs-web/assets/classDiagram-4FO5ZUOK-DzNdVs3v.js +0 -1
  168. package/dist/rcs-web/assets/classDiagram-v2-Q7XG4LA2-DzNdVs3v.js +0 -1
  169. package/dist/rcs-web/assets/main-DhbquBXW.css +0 -1
  170. package/dist/rcs-web/assets/main-N3GYCdLF.js +0 -260
  171. package/packages/remote-control-server/web/dist/assets/SessionDetail-D-xhkyLo.js +0 -65
  172. package/packages/remote-control-server/web/dist/assets/SessionVisibilityBadge-91WyR9K6.js +0 -11
  173. package/packages/remote-control-server/web/dist/assets/ShareLanding-DhwgJb6V.js +0 -1
  174. package/packages/remote-control-server/web/dist/assets/channel-CCfqpGSl.js +0 -1
  175. package/packages/remote-control-server/web/dist/assets/classDiagram-4FO5ZUOK-DzNdVs3v.js +0 -1
  176. package/packages/remote-control-server/web/dist/assets/classDiagram-v2-Q7XG4LA2-DzNdVs3v.js +0 -1
  177. package/packages/remote-control-server/web/dist/assets/main-DhbquBXW.css +0 -1
  178. package/packages/remote-control-server/web/dist/assets/main-N3GYCdLF.js +0 -260
@@ -181,6 +181,60 @@ describe('Session Attach wait mode', () => {
181
181
  expect(await res.json()).toEqual({ assigned: false })
182
182
  })
183
183
 
184
+ test('assignment: disconnected env + authorized caller → reactivated, not 404', async () => {
185
+ // A wait-mode worker whose env the disconnect monitor flipped (laptop
186
+ // sleep during the 24h wait, pre-fix claim-wait, etc.) must recover on
187
+ // its next poll instead of dead-waiting on swallowed 404s — the CLI
188
+ // ignores assignment errors until its deadline.
189
+ const env = storeCreateEnvironment({
190
+ secret: 'test-api-key',
191
+ machineName: 'mac1',
192
+ ownerUserId: owner.id,
193
+ sessionMode: 'attach',
194
+ })
195
+ storeUpdateEnvironment(env.id, { status: 'disconnected' })
196
+
197
+ const res = await app.request(`/v1/environments/${env.id}/assignment`, {
198
+ headers: { ...ADMIN_KEY },
199
+ })
200
+ expect(res.status).toBe(200)
201
+ expect(await res.json()).toEqual({ assigned: false })
202
+ expect(storeGetEnvironment(env.id)?.status).toBe('active')
203
+ })
204
+
205
+ test('assignment: disconnected env + stranger → 403, env NOT reactivated', async () => {
206
+ // Same invariant as the heartbeat test above: a 403 must not resurrect
207
+ // or keep alive an env the caller has no business touching.
208
+ const env = storeCreateEnvironment({
209
+ secret: 'test-api-key',
210
+ machineName: 'mac1',
211
+ ownerUserId: owner.id,
212
+ sessionMode: 'attach',
213
+ })
214
+ storeUpdateEnvironment(env.id, { status: 'disconnected' })
215
+ const stranger = createUser(db)
216
+
217
+ const res = await app.request(`/v1/environments/${env.id}/assignment`, {
218
+ headers: { Authorization: rctAuth(stranger.id) },
219
+ })
220
+ expect(res.status).toBe(403)
221
+ expect(storeGetEnvironment(env.id)?.status).toBe('disconnected')
222
+ })
223
+
224
+ test('assignment: deregistered env → 404 (terminal)', async () => {
225
+ const env = storeCreateEnvironment({
226
+ secret: 'test-api-key',
227
+ machineName: 'mac1',
228
+ ownerUserId: owner.id,
229
+ sessionMode: 'attach',
230
+ })
231
+ storeUpdateEnvironment(env.id, { status: 'deregistered' })
232
+ const res = await app.request(`/v1/environments/${env.id}/assignment`, {
233
+ headers: { ...ADMIN_KEY },
234
+ })
235
+ expect(res.status).toBe(404)
236
+ })
237
+
184
238
  test('assignment: session bound to the env (web attach) → assigned with session id', async () => {
185
239
  const env = storeCreateEnvironment({
186
240
  secret: 'test-api-key',
@@ -313,6 +313,43 @@ describe('POST /v1/sessions/:id/attach', () => {
313
313
  expect(body.error.type).toBe('environment_unowned')
314
314
  })
315
315
 
316
+ test('bare admin API key attaches to an unowned env (documented worker setup)', async () => {
317
+ // Self-hosted workers connect with SALUZI_BRIDGE_OAUTH_TOKEN=<RCS_API_KEYS>
318
+ // and no SALUZI_BRIDGE_USERNAME — the env they register is unowned until
319
+ // claimed, and the direct-attach path never prints a claim URL. A hard
320
+ // 409 here made `/rc --attach <id>` fail out of the box. Bare admin keys
321
+ // are trusted credentials (ensureAttachAccess already audited their
322
+ // session-manage bypass) — the bind is allowed.
323
+ const owner = createUser(db)
324
+ const env = storeCreateEnvironment({
325
+ secret: 'test-api-key',
326
+ machineName: 'cli-worker',
327
+ })
328
+ const session = createSession({
329
+ environment_id: null,
330
+ title: 'Web session',
331
+ source: 'web',
332
+ })
333
+ storeBindSession(session.id, owner.id, 'user')
334
+
335
+ const res = await app.request(`/v1/sessions/${session.id}/attach`, {
336
+ method: 'POST',
337
+ headers: { ...JSON_HDRS, ...ADMIN_KEY },
338
+ body: JSON.stringify({ environment_id: env.id }),
339
+ })
340
+ expect(res.status).toBe(200)
341
+ const body = (await res.json()) as {
342
+ status: string
343
+ session_id: string
344
+ work_item_id: string
345
+ rebind: boolean
346
+ }
347
+ expect(body.status).toBe('attached')
348
+ expect(body.session_id).toBe(session.id)
349
+ expect(body.work_item_id).toMatch(/^work_/)
350
+ expect(body.rebind).toBe(true)
351
+ })
352
+
316
353
  test('archived session → 409 session_closed', async () => {
317
354
  const world = seedWorld(db)
318
355
  const { archiveSession } = await import('../services/session')
@@ -15,6 +15,7 @@ export function createMockConfig(overrides?: Partial<Record<string, unknown>>) {
15
15
  webCorsOrigins: [],
16
16
  wsIdleTimeout: 30,
17
17
  wsKeepaliveInterval: 20,
18
+ artifactsPublicAccess: true,
18
19
  ...overrides,
19
20
  }
20
21
  }
@@ -10,8 +10,11 @@ import { Hono } from 'hono'
10
10
  import {
11
11
  storeReset,
12
12
  storeCreateSession,
13
+ storeCreateEnvironment,
13
14
  storeBindSession,
14
15
  storeListSessionsByOwnerUuid,
16
+ storeListSessionsByEnvironment,
17
+ storeGetActiveWorkItemBySession,
15
18
  } from '../store'
16
19
  import { initDatabase, resetDbSingleton } from '../db/sqlite'
17
20
  import { issueToken } from '../auth/token'
@@ -176,3 +179,95 @@ describe('M2: resolveOwnedWebSessionId removes redundant fallback', () => {
176
179
  expect(source).not.toMatch(/import\s*\{[^}]*storeIsSessionOwner[^}]*\}/)
177
180
  })
178
181
  })
182
+
183
+ // =============================================================================
184
+ // worker_at_capacity pre-validation: the repl-worker capacity check must run
185
+ // BEFORE createSession. It used to run after, where the just-created session
186
+ // (already bound to body.environment_id) always tripped it — every web session
187
+ // creation on a repl worker env 409'd AND left a stray bound-but-never-
188
+ // dispatched session behind on each retry.
189
+ // =============================================================================
190
+ describe('worker_at_capacity pre-validation (repl workers)', () => {
191
+ let app: Hono
192
+
193
+ beforeEach(() => {
194
+ freshDb()
195
+ storeReset()
196
+ app = createApp()
197
+ })
198
+
199
+ test('idle repl worker env → 200, session created AND work item dispatched', async () => {
200
+ const env = storeCreateEnvironment({
201
+ secret: 'test-api-key',
202
+ machineName: 'cli-worker',
203
+ workerType: 'repl',
204
+ })
205
+ const res = await app.request('/web/sessions', {
206
+ method: 'POST',
207
+ headers: {
208
+ Authorization: userAuth('webuser'),
209
+ 'Content-Type': 'application/json',
210
+ },
211
+ body: JSON.stringify({ title: 'On Worker', environment_id: env.id }),
212
+ })
213
+ expect(res.status).toBe(200)
214
+ const body = (await res.json()) as { id: string; environment_id: string }
215
+ expect(body.environment_id).toBe(env.id)
216
+ // Pre-fix the 409 returned before createWorkItem — the CLI would poll a
217
+ // session that never received work.
218
+ const work = storeGetActiveWorkItemBySession(body.id)
219
+ expect(work).not.toBeNull()
220
+ expect(work?.environmentId).toBe(env.id)
221
+ })
222
+
223
+ test('repl env with an open session → 409 and NO stray session left behind', async () => {
224
+ const env = storeCreateEnvironment({
225
+ secret: 'test-api-key',
226
+ machineName: 'cli-worker',
227
+ workerType: 'repl',
228
+ })
229
+ storeCreateSession({
230
+ environmentId: env.id,
231
+ title: 'Existing',
232
+ source: 'web',
233
+ })
234
+ const before = storeListSessionsByEnvironment(env.id).length
235
+
236
+ const res = await app.request('/web/sessions', {
237
+ method: 'POST',
238
+ headers: {
239
+ Authorization: userAuth('webuser'),
240
+ 'Content-Type': 'application/json',
241
+ },
242
+ body: JSON.stringify({ title: 'Overflow', environment_id: env.id }),
243
+ })
244
+ expect(res.status).toBe(409)
245
+ const body = (await res.json()) as { error: { type: string } }
246
+ expect(body.error.type).toBe('worker_at_capacity')
247
+ // Pre-fix the session record was created (and bound) before the 409.
248
+ expect(storeListSessionsByEnvironment(env.id).length).toBe(before)
249
+ })
250
+
251
+ test('non-repl (spawn) env skips the capacity check', async () => {
252
+ const env = storeCreateEnvironment({
253
+ secret: 'test-api-key',
254
+ machineName: 'spawn-worker',
255
+ workerType: 'claude_code',
256
+ maxSessions: 4,
257
+ })
258
+ storeCreateSession({
259
+ environmentId: env.id,
260
+ title: 'Existing',
261
+ source: 'web',
262
+ })
263
+ const res = await app.request('/web/sessions', {
264
+ method: 'POST',
265
+ headers: {
266
+ Authorization: userAuth('webuser'),
267
+ 'Content-Type': 'application/json',
268
+ },
269
+ body: JSON.stringify({ title: 'Second', environment_id: env.id }),
270
+ })
271
+ expect(res.status).toBe(200)
272
+ })
273
+ })
@@ -10,6 +10,9 @@ import {
10
10
  storeCreateSession,
11
11
  storeCreateEnvironment,
12
12
  storeBindSession,
13
+ storeUpdateEnvironment,
14
+ storeGetEnvironment,
15
+ storeGetActiveWorkItemBySession,
13
16
  } from '../store'
14
17
  import {
15
18
  removeEventBus,
@@ -323,6 +326,72 @@ describe('V1 Session Routes', () => {
323
326
  expect(body.id).toMatch(/^session_/)
324
327
  })
325
328
 
329
+ test('POST /v1/sessions with environment_id refreshes env liveness (claim-wait retry loop)', async () => {
330
+ // An unowned env's claim-wait loop retries POST /v1/sessions every 5s
331
+ // (409 environment_unowned) for up to 24h — that retry IS the worker's
332
+ // only traffic. Without touching lastPollAt here the disconnect monitor
333
+ // flips the env after RCS_DISCONNECT_TIMEOUT, and when the claim finally
334
+ // lands the work item dispatch rejects the non-active env: a session
335
+ // that exists yet never receives work (the CLI polls 204s forever).
336
+ ensureUser('testuser')
337
+ const envRes = await app.request('/v1/environments/bridge', {
338
+ method: 'POST',
339
+ headers: {
340
+ ...AUTH_HEADERS,
341
+ 'Content-Type': 'application/json',
342
+ 'X-User-Id': 'testuser',
343
+ },
344
+ body: JSON.stringify({ machine_name: 'test' }),
345
+ })
346
+ const { environment_id } = await envRes.json()
347
+ // Age the heartbeat past the disconnect timeout, then simulate one
348
+ // claim-wait retry.
349
+ const stale = new Date(Date.now() - 10 * 60 * 1000)
350
+ storeUpdateEnvironment(environment_id, { lastPollAt: stale })
351
+ const before = storeGetEnvironment(environment_id)?.lastPollAt?.getTime()
352
+
353
+ const sessRes = await app.request('/v1/sessions', {
354
+ method: 'POST',
355
+ headers: { ...AUTH_HEADERS, 'Content-Type': 'application/json' },
356
+ body: JSON.stringify({ environment_id }),
357
+ })
358
+ expect(sessRes.status).toBe(200)
359
+ const after = storeGetEnvironment(environment_id)?.lastPollAt?.getTime()
360
+ expect(after).not.toBe(before)
361
+ expect(after ?? 0).toBeGreaterThan(stale.getTime())
362
+ })
363
+
364
+ test('POST /v1/sessions reactivates a monitor-flipped env so the work item is created', async () => {
365
+ // Recovery path for envs already flipped to 'disconnected' (CLI waited
366
+ // out the timeout before these fixes, laptop sleep, etc.): the retry
367
+ // reactivates the env, so the session gets its work item instead of
368
+ // becoming a zombie the CLI polls forever.
369
+ ensureUser('testuser')
370
+ const envRes = await app.request('/v1/environments/bridge', {
371
+ method: 'POST',
372
+ headers: {
373
+ ...AUTH_HEADERS,
374
+ 'Content-Type': 'application/json',
375
+ 'X-User-Id': 'testuser',
376
+ },
377
+ body: JSON.stringify({ machine_name: 'test' }),
378
+ })
379
+ const { environment_id } = await envRes.json()
380
+ storeUpdateEnvironment(environment_id, { status: 'disconnected' })
381
+
382
+ const sessRes = await app.request('/v1/sessions', {
383
+ method: 'POST',
384
+ headers: { ...AUTH_HEADERS, 'Content-Type': 'application/json' },
385
+ body: JSON.stringify({ environment_id }),
386
+ })
387
+ expect(sessRes.status).toBe(200)
388
+ const body = await sessRes.json()
389
+ expect(storeGetEnvironment(environment_id)?.status).toBe('active')
390
+ const work = storeGetActiveWorkItemBySession(body.id)
391
+ expect(work).not.toBeNull()
392
+ expect(work?.environmentId).toBe(environment_id)
393
+ })
394
+
326
395
  test('POST /v1/sessions with events — publishes initial events', async () => {
327
396
  const sessRes = await app.request('/v1/sessions', {
328
397
  method: 'POST',
@@ -34,6 +34,12 @@ export const config = {
34
34
  process.env.RCS_WS_CLIENT_INACTIVITY_TIMEOUT || '300',
35
35
  10,
36
36
  ),
37
+ /** Whether artifact content (GET /v1/artifacts/:id/content) is served
38
+ * without authentication. Default true — the unguessable artifact id is
39
+ * the secret, same trust model as the upstream cloud-artifacts service.
40
+ * Set RCS_ARTIFACTS_PUBLIC_ACCESS=false to gate reads behind web session
41
+ * or worker credentials. */
42
+ artifactsPublicAccess: process.env.RCS_ARTIFACTS_PUBLIC_ACCESS !== 'false',
37
43
  } as const
38
44
 
39
45
  export function getBaseUrl(): string {
@@ -119,6 +119,26 @@ CREATE TABLE IF NOT EXISTS session_events (
119
119
  created_at INTEGER NOT NULL
120
120
  );
121
121
 
122
+ -- Artifacts — self-hosted HTML pages uploaded by the CLI `artifact` tool or
123
+ -- created from the web UI gallery. `id` doubles as the unguessable share
124
+ -- slug (URL-is-the-secret model, same as the upstream cloud-artifacts
125
+ -- service). `team_id` is denormalized from session ownership at upload time
126
+ -- so the team gallery can filter without joining session_owners.
127
+ CREATE TABLE IF NOT EXISTS artifacts (
128
+ id TEXT PRIMARY KEY,
129
+ session_id TEXT,
130
+ team_id TEXT,
131
+ title TEXT,
132
+ content_type TEXT NOT NULL DEFAULT 'text/html',
133
+ content TEXT NOT NULL,
134
+ size INTEGER NOT NULL,
135
+ version INTEGER NOT NULL DEFAULT 1,
136
+ created_by TEXT,
137
+ created_at TEXT NOT NULL,
138
+ updated_at TEXT NOT NULL,
139
+ expires_at TEXT
140
+ );
141
+
122
142
  -- Performance indexes
123
143
  CREATE INDEX IF NOT EXISTS idx_session_owners_owner ON session_owners(owner_type, owner_id);
124
144
  CREATE INDEX IF NOT EXISTS idx_session_owners_session ON session_owners(session_id);
@@ -134,3 +154,6 @@ CREATE INDEX IF NOT EXISTS idx_environments_team ON environments(team_id);
134
154
  CREATE INDEX IF NOT EXISTS idx_environments_owner_user ON environments(owner_user_id);
135
155
  CREATE INDEX IF NOT EXISTS idx_environments_claim_token ON environments(claim_token);
136
156
  CREATE INDEX IF NOT EXISTS idx_session_events_session ON session_events(session_id);
157
+ CREATE INDEX IF NOT EXISTS idx_artifacts_session ON artifacts(session_id);
158
+ CREATE INDEX IF NOT EXISTS idx_artifacts_team ON artifacts(team_id);
159
+ CREATE INDEX IF NOT EXISTS idx_artifacts_expires ON artifacts(expires_at);
@@ -12,6 +12,7 @@ import { webCorsOptions } from './auth/cors'
12
12
  import { websocket } from './transport/ws-shared'
13
13
  import { getDb } from './db/sqlite'
14
14
  import { reapExpiredSessions } from './auth/session'
15
+ import { cleanupExpiredArtifacts } from './services/artifacts'
15
16
 
16
17
  console.log('[RCS] In-memory store ready (no SQLite)')
17
18
 
@@ -66,11 +67,19 @@ try {
66
67
  console.warn('[RCS] Failed to reap expired sessions:', err)
67
68
  }
68
69
 
70
+ // Clean up expired artifacts (7/30-day TTLs) on the same hourly cadence
71
+ try {
72
+ cleanupExpiredArtifacts()
73
+ } catch (err) {
74
+ console.warn('[RCS] Failed to clean up expired artifacts:', err)
75
+ }
76
+
69
77
  const reapInterval = setInterval(
70
78
  () => {
71
79
  try {
72
80
  const db = getDb()
73
81
  reapExpiredSessions(db)
82
+ cleanupExpiredArtifacts()
74
83
  } catch {
75
84
  // ignore — DB may not be available
76
85
  }
@@ -9,6 +9,7 @@ import { createSessionIngressApp } from './routes/v1/session-ingress'
9
9
  import { csrfCheck } from './auth/csrf'
10
10
 
11
11
  // Routes
12
+ import v1Artifacts from './routes/v1/artifacts'
12
13
  import v1Environments from './routes/v1/environments'
13
14
  import v1EnvironmentsWork from './routes/v1/environments.work'
14
15
  import v1Sessions from './routes/v1/sessions'
@@ -16,6 +17,7 @@ import v2CodeSessions from './routes/v2/code-sessions'
16
17
  import v2Worker from './routes/v2/worker'
17
18
  import v2WorkerEventsStream from './routes/v2/worker-events-stream'
18
19
  import v2WorkerEvents from './routes/v2/worker-events'
20
+ import webArtifacts from './routes/web/artifacts'
19
21
  import webAuth from './routes/web/auth'
20
22
  import webAuthRoutes from './routes/web/auth-routes'
21
23
  import webSessions from './routes/web/sessions'
@@ -58,6 +60,8 @@ export function registerRoutes(
58
60
  app.get('/code/:sessionId', ss({ root: webDir, path: 'index.html' }))
59
61
  // /code/s/:shareToken — share landing page (two-segment path, not matched by /code/:sessionId)
60
62
  app.get('/code/s/:shareToken', ss({ root: webDir, path: 'index.html' }))
63
+ // /code/a/:artifactId — artifact viewer page (two-segment path)
64
+ app.get('/code/a/:artifactId', ss({ root: webDir, path: 'index.html' }))
61
65
  // /code/claim/:claimToken — environment claim landing page
62
66
  app.get('/code/claim/:claimToken', ss({ root: webDir, path: 'index.html' }))
63
67
 
@@ -65,6 +69,9 @@ export function registerRoutes(
65
69
  app.route('/v1/environments', v1Environments)
66
70
  app.route('/v1/environments', v1EnvironmentsWork)
67
71
 
72
+ // v1 Artifacts — CLI worker upload + public content read
73
+ app.route('/v1', v1Artifacts)
74
+
68
75
  // v1 Session routes
69
76
  app.route('/v1/sessions', v1Sessions)
70
77
 
@@ -84,6 +91,7 @@ export function registerRoutes(
84
91
  app.use('/web/*', csrfCheck)
85
92
  app.route('/web', webAuth)
86
93
  app.route('/web/auth', webAuthRoutes)
94
+ app.route('/web', webArtifacts)
87
95
  app.route('/web', webShares)
88
96
  app.route('/web', webTeams)
89
97
  app.route('/web', webSessions)
@@ -0,0 +1,179 @@
1
+ import { Hono } from 'hono'
2
+ import type { Context, Next } from 'hono'
3
+ import { log } from '../../logger'
4
+ import { config, getBaseUrl } from '../../config'
5
+ import { validateApiKey } from '../../auth/api-key'
6
+ import { verifyWorkerJwt } from '../../auth/jwt'
7
+ import { extractBearerToken, sessionAuth } from '../../auth/middleware'
8
+ import { resolveExistingSessionId } from '../../services/session'
9
+ import {
10
+ ArtifactError,
11
+ ARTIFACT_ALLOWED_TTLS,
12
+ ARTIFACT_MAX_BYTES,
13
+ createArtifact,
14
+ getArtifactContent,
15
+ type ArtifactTtl,
16
+ } from '../../services/artifacts'
17
+
18
+ const app = new Hono()
19
+
20
+ /**
21
+ * CLI/worker artifact upload + public content read.
22
+ *
23
+ * Mirrors the upstream cloud-artifacts worker contract:
24
+ * - POST /v1/artifacts (API key or worker JWT auth) — body {html, hash?, ttl?,
25
+ * session_id?, title?} → {id, url, expiresAt}. Passing `hash` overwrites
26
+ * the artifact in place (URL stays stable).
27
+ * - GET /v1/artifacts/:id/content — serves the stored HTML document. Access
28
+ * is URL-is-the-secret (the id is an unguessable nanoid), same trust model
29
+ * as the upstream service. Set RCS_ARTIFACTS_PUBLIC_ACCESS=false to require
30
+ * auth on content reads as well.
31
+ */
32
+
33
+ function artifactUrl(id: string): string {
34
+ return `${getBaseUrl()}/v1/artifacts/${id}/content`
35
+ }
36
+
37
+ /** API key or worker JWT — same credential surface as session ingress. */
38
+ function authenticateWorker(c: Context): boolean {
39
+ const token = extractBearerToken(c)
40
+ if (!token) return false
41
+ if (validateApiKey(token)) return true
42
+ return verifyWorkerJwt(token) !== null
43
+ }
44
+
45
+ /**
46
+ * Content-read auth. Default (artifactsPublicAccess=true): open — the
47
+ * unguessable id IS the secret, matching the upstream cloud-artifacts trust
48
+ * model. Operators can set RCS_ARTIFACTS_PUBLIC_ACCESS=false to require a
49
+ * web session (cookie/bearer) or worker credential instead.
50
+ */
51
+ async function contentAuth(c: Context, next: Next) {
52
+ if (config.artifactsPublicAccess === false) {
53
+ if (authenticateWorker(c)) {
54
+ await next()
55
+ return
56
+ }
57
+ return sessionAuth(c, next)
58
+ }
59
+ await next()
60
+ }
61
+
62
+ app.post('/artifacts', async c => {
63
+ if (!authenticateWorker(c)) {
64
+ return c.json(
65
+ { error: { type: 'unauthorized', message: 'Invalid auth token' } },
66
+ 401,
67
+ )
68
+ }
69
+
70
+ let body: Record<string, unknown>
71
+ try {
72
+ body = await c.req.json()
73
+ } catch {
74
+ return c.json(
75
+ { error: { type: 'bad_request', message: 'Invalid JSON body' } },
76
+ 400,
77
+ )
78
+ }
79
+
80
+ const html = typeof body.html === 'string' ? body.html : undefined
81
+ if (!html || html.length === 0) {
82
+ return c.json(
83
+ { error: { type: 'bad_request', message: 'html is required' } },
84
+ 400,
85
+ )
86
+ }
87
+
88
+ const hash = typeof body.hash === 'string' ? body.hash : undefined
89
+ const ttlRaw = body.ttl
90
+ let ttl: ArtifactTtl | undefined
91
+ if (ttlRaw !== undefined) {
92
+ const n =
93
+ typeof ttlRaw === 'number' ? ttlRaw : Number.parseInt(String(ttlRaw), 10)
94
+ if (!ARTIFACT_ALLOWED_TTLS.includes(n as ArtifactTtl)) {
95
+ return c.json(
96
+ { error: { type: 'bad_request', message: 'ttl must be 7 or 30' } },
97
+ 400,
98
+ )
99
+ }
100
+ ttl = n as ArtifactTtl
101
+ }
102
+
103
+ const title = typeof body.title === 'string' ? body.title : undefined
104
+
105
+ // Session association is best-effort: unknown session ids (local REPL runs,
106
+ // SDK mode) simply leave the artifact session-less.
107
+ let sessionId: string | undefined
108
+ const requestedSessionId =
109
+ typeof body.session_id === 'string' ? body.session_id : undefined
110
+ if (requestedSessionId) {
111
+ sessionId = resolveExistingSessionId(requestedSessionId) ?? undefined
112
+ if (!sessionId) {
113
+ log(
114
+ `[artifacts] upload referenced unknown session ${requestedSessionId} — storing session-less`,
115
+ )
116
+ }
117
+ }
118
+
119
+ try {
120
+ const result = createArtifact({
121
+ html,
122
+ hash,
123
+ ttl,
124
+ sessionId,
125
+ title,
126
+ })
127
+ return c.json(
128
+ {
129
+ id: result.id,
130
+ url: artifactUrl(result.id),
131
+ expiresAt: result.expiresAt,
132
+ },
133
+ 201,
134
+ )
135
+ } catch (err) {
136
+ if (err instanceof ArtifactError) {
137
+ const status =
138
+ err.code === 'payload_too_large'
139
+ ? 413
140
+ : err.code === 'invalid_ttl' || err.code === 'invalid_hash'
141
+ ? 400
142
+ : 500
143
+ return c.json({ error: { type: err.code, message: err.message } }, status)
144
+ }
145
+ throw err
146
+ }
147
+ })
148
+
149
+ /** Content read — URL-is-the-secret by default; session-auth-gated when public access is disabled. */
150
+ app.get('/artifacts/:id/content', contentAuth, async c => {
151
+ const id = c.req.param('id')!
152
+ const artifact = getArtifactContent(id)
153
+ if (!artifact) {
154
+ return c.notFound()
155
+ }
156
+
157
+ return c.body(artifact.content, 200, {
158
+ 'content-type': artifact.contentType,
159
+ 'cache-control': 'public, max-age=60',
160
+ 'x-content-type-options': 'nosniff',
161
+ })
162
+ })
163
+
164
+ /** Head variant so embedders can probe existence cheaply. */
165
+ app.on('HEAD', '/artifacts/:id/content', contentAuth, async c => {
166
+ const id = c.req.param('id')!
167
+ const artifact = getArtifactContent(id)
168
+ if (!artifact) return c.notFound()
169
+ return c.body(null, 200, {
170
+ 'content-type': artifact.contentType,
171
+ 'cache-control': 'public, max-age=60',
172
+ })
173
+ })
174
+
175
+ // Cap documented alongside the service — kept here so route-level callers
176
+ // (tests) can assert against the same constant.
177
+ export { ARTIFACT_MAX_BYTES }
178
+
179
+ export default app
@@ -4,7 +4,7 @@ import {
4
4
  registerEnvironment,
5
5
  deregisterEnvironment,
6
6
  reconnectEnvironment,
7
- updatePollTime,
7
+ touchEnvironmentActivity,
8
8
  } from '../../services/environment'
9
9
  import { acceptCliHeaders, apiKeyAuth } from '../../auth/middleware'
10
10
  import {
@@ -291,7 +291,12 @@ app.post('/bridge', acceptCliHeaders, bridgeAuth, async c => {
291
291
  app.get('/:id/assignment', acceptCliHeaders, bridgeAuth, async c => {
292
292
  const envId = c.req.param('id')!
293
293
  const env = storeGetEnvironment(envId)
294
- if (!env || env.status !== 'active') {
294
+ // 'disconnected' is NOT a 404 here: the ownership gate below authorizes the
295
+ // caller first, then touchEnvironmentActivity reactivates the env — a live
296
+ // wait-mode worker polling a monitor-flipped env recovers instead of
297
+ // dead-waiting on swallowed 404s for its 24h deadline. Deregistered is
298
+ // terminal.
299
+ if (!env || env.status === 'deregistered') {
295
300
  return c.json(
296
301
  { error: { type: 'not_found', message: 'Environment not found' } },
297
302
  404,
@@ -299,10 +304,11 @@ app.get('/:id/assignment', acceptCliHeaders, bridgeAuth, async c => {
299
304
  }
300
305
 
301
306
  // Ownership gate: assignment discloses the env's bound session AND doubles
302
- // as its liveness signal (updatePollTime below) — both are restricted to
303
- // the env owner (the wait-mode worker polls under the same identity it
304
- // registered with) and admins. Unowned envs (claim pending) stay open:
305
- // nothing is bound yet, so there is nothing to disclose or keep alive.
307
+ // as its liveness signal (touchEnvironmentActivity below) — both are
308
+ // restricted to the env owner (the wait-mode worker polls under the same
309
+ // identity it registered with) and admins. Unowned envs (claim pending)
310
+ // stay open: nothing is bound yet, so there is nothing to disclose or keep
311
+ // alive.
306
312
  const caller = resolveAttachCaller(c)
307
313
  if (caller.dbUnavailable) {
308
314
  return c.json(
@@ -345,9 +351,11 @@ app.get('/:id/assignment', acceptCliHeaders, bridgeAuth, async c => {
345
351
  // The attach-wait worker has no work item in flight, so the work-poll
346
352
  // route never refreshes its heartbeat. This poll IS its liveness signal:
347
353
  // without touching lastPollAt the disconnect monitor flips the env to
348
- // disconnected after the idle timeout, assignment starts 404ing, and the
349
- // CLI silently spins until its 24h deadline (P0-2).
350
- updatePollTime(envId)
354
+ // disconnected after the idle timeout and the CLI silently spins until its
355
+ // 24h deadline (P0-2). touchEnvironmentActivity also reactivates an env the
356
+ // monitor already flipped (runs only after the gate — a 403 must not keep
357
+ // a dead env looking alive).
358
+ touchEnvironmentActivity(envId)
351
359
  const assigned = storeListSessionsByEnvironment(envId)
352
360
  .filter(s => !isSessionClosedStatus(s.status))
353
361
  .filter(s => s.source !== 'acp')
@@ -6,14 +6,14 @@ import {
6
6
  heartbeatWork,
7
7
  } from '../../services/work-dispatch'
8
8
  import { apiKeyAuth, acceptCliHeaders } from '../../auth/middleware'
9
- import { updatePollTime } from '../../services/environment'
9
+ import { touchEnvironmentActivity } from '../../services/environment'
10
10
 
11
11
  const app = new Hono()
12
12
 
13
13
  /** GET /v1/environments/:id/work/poll — Long-poll for work */
14
14
  app.get('/:id/work/poll', acceptCliHeaders, apiKeyAuth, async c => {
15
15
  const envId = c.req.param('id')!
16
- updatePollTime(envId)
16
+ touchEnvironmentActivity(envId)
17
17
  const result = await pollWork(envId)
18
18
  if (!result) {
19
19
  // Return 204 No Content so the client's axios parses it as null