@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.
- package/dist/cli.js +534 -349
- package/dist/guide/guide-data.json +8 -0
- package/dist/rcs-schema.sql +23 -0
- package/dist/rcs-web/assets/{AdminPanel-CqP_94xa.js → AdminPanel-94uchXH7.js} +1 -1
- package/dist/rcs-web/assets/ArtifactView-CT0Fu4U5.js +6 -0
- package/dist/rcs-web/assets/ArtifactsGallery-DgK1CSno.js +339 -0
- package/{packages/remote-control-server/web/dist/assets/Claim-BvfARQ6Q.js → dist/rcs-web/assets/Claim-C7V1Q01Y.js} +1 -1
- package/dist/rcs-web/assets/{Dashboard-BDGi4XpN.js → Dashboard-NLM7Kgm8.js} +2 -2
- package/{packages/remote-control-server/web/dist/assets/Join-PPDYBhsJ.js → dist/rcs-web/assets/Join-BhjsvbAP.js} +1 -1
- package/dist/rcs-web/assets/{Login-JprQIEpr.js → Login-DRFE4F-F.js} +1 -1
- package/dist/rcs-web/assets/{Register-cuGuAE83.js → Register-FgLOFB4h.js} +1 -1
- package/dist/rcs-web/assets/SessionDetail-3P77L-sJ.js +60 -0
- package/dist/rcs-web/assets/SessionVisibilityBadge-raEV1sW8.js +11 -0
- package/dist/rcs-web/assets/{Setup-Buwa9Y3M.js → Setup-DDJHMu0z.js} +1 -1
- package/dist/rcs-web/assets/ShareLanding-DG5JbaWg.js +1 -0
- package/dist/rcs-web/assets/{TeamCreate-CaoinTSG.js → TeamCreate-D_AnatYF.js} +1 -1
- package/dist/rcs-web/assets/{TeamDetail-D5ahen24.js → TeamDetail-C1FFiAj2.js} +2 -2
- package/dist/rcs-web/assets/{TeamList-qCuKKSH3.js → TeamList-tfsnwvZG.js} +1 -1
- package/dist/rcs-web/assets/{TeamSettings-BaIwImrp.js → TeamSettings-FeTuY9_I.js} +1 -1
- package/dist/rcs-web/assets/{UserSearchInput-BxcYR_HU.js → UserSearchInput-Bu3RXDYh.js} +1 -1
- package/dist/rcs-web/assets/{UserSettings-zfGfcuWs.js → UserSettings-BrjmELAZ.js} +1 -1
- package/dist/rcs-web/assets/{arc-DwAbF6eM.js → arc-Di9w8tGK.js} +1 -1
- package/dist/rcs-web/assets/{architectureDiagram-3BPJPVTR-uMOwOwiN.js → architectureDiagram-3BPJPVTR-FT-IsfVA.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/blockDiagram-GPEHLZMM-B5BqFTkP.js → dist/rcs-web/assets/blockDiagram-GPEHLZMM-DryBvbP3.js} +1 -1
- package/dist/rcs-web/assets/{c4Diagram-AAUBKEIU-D6eienz2.js → c4Diagram-AAUBKEIU-HWAmxH56.js} +1 -1
- package/dist/rcs-web/assets/channel-N4W03sy5.js +1 -0
- package/dist/rcs-web/assets/{chunk-2J33WTMH-DqVxv8yW.js → chunk-2J33WTMH-AdjHpC7B.js} +1 -1
- package/dist/rcs-web/assets/{chunk-4BX2VUAB-X5o4x8om.js → chunk-4BX2VUAB-CpZ-59LR.js} +1 -1
- package/dist/rcs-web/assets/{chunk-55IACEB6-D7G25_Y5.js → chunk-55IACEB6-BBSmURaz.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/chunk-727SXJPM-CoXI1FIy.js → dist/rcs-web/assets/chunk-727SXJPM-1k6tgn8s.js} +1 -1
- package/dist/rcs-web/assets/{chunk-AQP2D5EJ-C73Crhqe.js → chunk-AQP2D5EJ-im0ij4Kj.js} +1 -1
- package/dist/rcs-web/assets/{chunk-FMBD7UC4-DVzxIsSE.js → chunk-FMBD7UC4-Cs3z8_hw.js} +1 -1
- package/dist/rcs-web/assets/{chunk-ND2GUHAM-C6_oEhLf.js → chunk-ND2GUHAM-BA9iE2wc.js} +1 -1
- package/dist/rcs-web/assets/{chunk-QZHKN3VN-COvH7k5r.js → chunk-QZHKN3VN-DbvzoKsw.js} +1 -1
- package/dist/rcs-web/assets/classDiagram-4FO5ZUOK-CcBgewcu.js +1 -0
- package/dist/rcs-web/assets/classDiagram-v2-Q7XG4LA2-CcBgewcu.js +1 -0
- package/dist/rcs-web/assets/{code-block-IT6T5CEO-CE-2f_-K.js → code-block-IT6T5CEO-wgmiR0vF.js} +1 -1
- package/dist/rcs-web/assets/{copy-9pzb1Po6.js → copy-BD6qrRfd.js} +1 -1
- package/dist/rcs-web/assets/{cose-bilkent-S5V4N54A-B9sMDFRe.js → cose-bilkent-S5V4N54A-DT4C5Yz2.js} +1 -1
- package/dist/rcs-web/assets/{dagre-BM42HDAG-DB1jXylM.js → dagre-BM42HDAG-CRf1H6Hv.js} +1 -1
- package/dist/rcs-web/assets/{diagram-2AECGRRQ-MNuqSRmg.js → diagram-2AECGRRQ-BChRpS-u.js} +1 -1
- package/dist/rcs-web/assets/{diagram-5GNKFQAL-BZwhvaEY.js → diagram-5GNKFQAL-BicbIYus.js} +1 -1
- package/dist/rcs-web/assets/{diagram-KO2AKTUF-3Wp2PnI2.js → diagram-KO2AKTUF-DCfQNjGq.js} +1 -1
- package/dist/rcs-web/assets/{diagram-LMA3HP47-CTcV1SBj.js → diagram-LMA3HP47-mMyd4aUd.js} +1 -1
- package/dist/rcs-web/assets/{diagram-OG6HWLK6-DzXhSQDC.js → diagram-OG6HWLK6-DUWhyWTm.js} +1 -1
- package/dist/rcs-web/assets/dialog-DympH6_o.js +1 -0
- package/dist/rcs-web/assets/{erDiagram-TEJ5UH35-CwSihkB8.js → erDiagram-TEJ5UH35-DnBH3GMK.js} +1 -1
- package/dist/rcs-web/assets/external-link-J-x6ty1h.js +6 -0
- package/dist/rcs-web/assets/{flowDiagram-I6XJVG4X-BSuXctT4.js → flowDiagram-I6XJVG4X-Ba7lo6ds.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/ganttDiagram-6RSMTGT7-B87oxjmY.js → dist/rcs-web/assets/ganttDiagram-6RSMTGT7-CKkj67eu.js} +1 -1
- package/dist/rcs-web/assets/{gitGraphDiagram-PVQCEYII-BRrwOi3C.js → gitGraphDiagram-PVQCEYII-205YdSRe.js} +1 -1
- package/dist/rcs-web/assets/{infoDiagram-5YYISTIA-B-8QXS0n.js → infoDiagram-5YYISTIA-CpPvGigJ.js} +1 -1
- package/dist/rcs-web/assets/{ishikawaDiagram-YF4QCWOH-Bau9MU6C.js → ishikawaDiagram-YF4QCWOH-D0ZEWQy7.js} +1 -1
- package/dist/rcs-web/assets/{journeyDiagram-JHISSGLW-D_tAp--R.js → journeyDiagram-JHISSGLW-DVl0M9a7.js} +1 -1
- package/dist/rcs-web/assets/{kanban-definition-UN3LZRKU---UIvjMZ.js → kanban-definition-UN3LZRKU-Dkr8uJC-.js} +1 -1
- package/dist/rcs-web/assets/{linear-CIDLjd5h.js → linear-CIdvseqY.js} +1 -1
- package/dist/rcs-web/assets/main-B8hkVvfg.js +265 -0
- package/dist/rcs-web/assets/main-C2O-g177.css +1 -0
- package/dist/rcs-web/assets/{mermaid-VLURNSYL-Cu5A5sMk.js → mermaid-VLURNSYL-DdTSeoZS.js} +4 -4
- package/dist/rcs-web/assets/{mermaid.core-CEJOtCGw.js → mermaid.core-CtvzCSq2.js} +5 -5
- package/{packages/remote-control-server/web/dist/assets/mindmap-definition-RKZ34NQL-ldyccyCP.js → dist/rcs-web/assets/mindmap-definition-RKZ34NQL-CMlDCaV_.js} +1 -1
- package/dist/rcs-web/assets/{pieDiagram-4H26LBE5-CmghofeF.js → pieDiagram-4H26LBE5-C6xMizTL.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/quadrantDiagram-W4KKPZXB-B-2N2UHr.js → dist/rcs-web/assets/quadrantDiagram-W4KKPZXB-D6epraE1.js} +1 -1
- package/dist/rcs-web/assets/{requirementDiagram-4Y6WPE33-39jlO9Y8.js → requirementDiagram-4Y6WPE33-BHmML6bk.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/sankeyDiagram-5OEKKPKP-DdY7KNFB.js → dist/rcs-web/assets/sankeyDiagram-5OEKKPKP-CIKij54Y.js} +1 -1
- package/dist/rcs-web/assets/{sequenceDiagram-3UESZ5HK-D2k5rX6x.js → sequenceDiagram-3UESZ5HK-CY0y5zdR.js} +1 -1
- package/dist/rcs-web/assets/{stateDiagram-AJRCARHV-CP9N457o.js → stateDiagram-AJRCARHV-FBhNz1Lc.js} +1 -1
- package/dist/rcs-web/assets/{stateDiagram-v2-BHNVJYJU-CPhZYlL0.js → stateDiagram-v2-BHNVJYJU-m7ruBTu5.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/timeline-definition-PNZ67QCA-DSEPBO8O.js → dist/rcs-web/assets/timeline-definition-PNZ67QCA-CJopHs1x.js} +1 -1
- package/dist/rcs-web/assets/{vennDiagram-CIIHVFJN-CkNMwCJ3.js → vennDiagram-CIIHVFJN-DF_uHOP7.js} +1 -1
- package/dist/rcs-web/assets/{wardleyDiagram-YWT4CUSO-Cxd-F60s.js → wardleyDiagram-YWT4CUSO-DQmjIZJA.js} +1 -1
- package/{packages/remote-control-server/web/dist/assets/xychartDiagram-2RQKCTM6-BLL105NN.js → dist/rcs-web/assets/xychartDiagram-2RQKCTM6-v8zL-j4G.js} +1 -1
- package/dist/rcs-web/index.html +2 -2
- package/package.json +1 -1
- package/packages/remote-control-server/src/__tests__/artifacts.test.ts +370 -0
- package/packages/remote-control-server/src/__tests__/attach-wait.test.ts +54 -0
- package/packages/remote-control-server/src/__tests__/attach.test.ts +37 -0
- package/packages/remote-control-server/src/__tests__/helpers/mock-config.ts +1 -0
- package/packages/remote-control-server/src/__tests__/low-priority-fixes.test.ts +95 -0
- package/packages/remote-control-server/src/__tests__/routes.test.ts +69 -0
- package/packages/remote-control-server/src/config.ts +6 -0
- package/packages/remote-control-server/src/db/rcs-schema.sql +23 -0
- package/packages/remote-control-server/src/index.ts +9 -0
- package/packages/remote-control-server/src/registerRoutes.ts +8 -0
- package/packages/remote-control-server/src/routes/v1/artifacts.ts +179 -0
- package/packages/remote-control-server/src/routes/v1/environments.ts +17 -9
- package/packages/remote-control-server/src/routes/v1/environments.work.ts +2 -2
- package/packages/remote-control-server/src/routes/v1/sessions.ts +39 -9
- package/packages/remote-control-server/src/routes/web/artifacts.ts +249 -0
- package/packages/remote-control-server/src/routes/web/sessions.ts +25 -22
- package/packages/remote-control-server/src/services/artifacts.ts +378 -0
- package/packages/remote-control-server/src/services/environment.ts +25 -0
- package/packages/remote-control-server/web/dist/assets/{AdminPanel-CqP_94xa.js → AdminPanel-94uchXH7.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/ArtifactView-CT0Fu4U5.js +6 -0
- package/packages/remote-control-server/web/dist/assets/ArtifactsGallery-DgK1CSno.js +339 -0
- package/{dist/rcs-web/assets/Claim-BvfARQ6Q.js → packages/remote-control-server/web/dist/assets/Claim-C7V1Q01Y.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{Dashboard-BDGi4XpN.js → Dashboard-NLM7Kgm8.js} +2 -2
- package/{dist/rcs-web/assets/Join-PPDYBhsJ.js → packages/remote-control-server/web/dist/assets/Join-BhjsvbAP.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{Login-JprQIEpr.js → Login-DRFE4F-F.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{Register-cuGuAE83.js → Register-FgLOFB4h.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/SessionDetail-3P77L-sJ.js +60 -0
- package/packages/remote-control-server/web/dist/assets/SessionVisibilityBadge-raEV1sW8.js +11 -0
- package/packages/remote-control-server/web/dist/assets/{Setup-Buwa9Y3M.js → Setup-DDJHMu0z.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/ShareLanding-DG5JbaWg.js +1 -0
- package/packages/remote-control-server/web/dist/assets/{TeamCreate-CaoinTSG.js → TeamCreate-D_AnatYF.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{TeamDetail-D5ahen24.js → TeamDetail-C1FFiAj2.js} +2 -2
- package/packages/remote-control-server/web/dist/assets/{TeamList-qCuKKSH3.js → TeamList-tfsnwvZG.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{TeamSettings-BaIwImrp.js → TeamSettings-FeTuY9_I.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{UserSearchInput-BxcYR_HU.js → UserSearchInput-Bu3RXDYh.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{UserSettings-zfGfcuWs.js → UserSettings-BrjmELAZ.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{arc-DwAbF6eM.js → arc-Di9w8tGK.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{architectureDiagram-3BPJPVTR-uMOwOwiN.js → architectureDiagram-3BPJPVTR-FT-IsfVA.js} +1 -1
- package/{dist/rcs-web/assets/blockDiagram-GPEHLZMM-B5BqFTkP.js → packages/remote-control-server/web/dist/assets/blockDiagram-GPEHLZMM-DryBvbP3.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{c4Diagram-AAUBKEIU-D6eienz2.js → c4Diagram-AAUBKEIU-HWAmxH56.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/channel-N4W03sy5.js +1 -0
- package/packages/remote-control-server/web/dist/assets/{chunk-2J33WTMH-DqVxv8yW.js → chunk-2J33WTMH-AdjHpC7B.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-4BX2VUAB-X5o4x8om.js → chunk-4BX2VUAB-CpZ-59LR.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-55IACEB6-D7G25_Y5.js → chunk-55IACEB6-BBSmURaz.js} +1 -1
- package/{dist/rcs-web/assets/chunk-727SXJPM-CoXI1FIy.js → packages/remote-control-server/web/dist/assets/chunk-727SXJPM-1k6tgn8s.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-AQP2D5EJ-C73Crhqe.js → chunk-AQP2D5EJ-im0ij4Kj.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-FMBD7UC4-DVzxIsSE.js → chunk-FMBD7UC4-Cs3z8_hw.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-ND2GUHAM-C6_oEhLf.js → chunk-ND2GUHAM-BA9iE2wc.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{chunk-QZHKN3VN-COvH7k5r.js → chunk-QZHKN3VN-DbvzoKsw.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/classDiagram-4FO5ZUOK-CcBgewcu.js +1 -0
- package/packages/remote-control-server/web/dist/assets/classDiagram-v2-Q7XG4LA2-CcBgewcu.js +1 -0
- package/packages/remote-control-server/web/dist/assets/{code-block-IT6T5CEO-CE-2f_-K.js → code-block-IT6T5CEO-wgmiR0vF.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{copy-9pzb1Po6.js → copy-BD6qrRfd.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{cose-bilkent-S5V4N54A-B9sMDFRe.js → cose-bilkent-S5V4N54A-DT4C5Yz2.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{dagre-BM42HDAG-DB1jXylM.js → dagre-BM42HDAG-CRf1H6Hv.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{diagram-2AECGRRQ-MNuqSRmg.js → diagram-2AECGRRQ-BChRpS-u.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{diagram-5GNKFQAL-BZwhvaEY.js → diagram-5GNKFQAL-BicbIYus.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{diagram-KO2AKTUF-3Wp2PnI2.js → diagram-KO2AKTUF-DCfQNjGq.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{diagram-LMA3HP47-CTcV1SBj.js → diagram-LMA3HP47-mMyd4aUd.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{diagram-OG6HWLK6-DzXhSQDC.js → diagram-OG6HWLK6-DUWhyWTm.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/dialog-DympH6_o.js +1 -0
- package/packages/remote-control-server/web/dist/assets/{erDiagram-TEJ5UH35-CwSihkB8.js → erDiagram-TEJ5UH35-DnBH3GMK.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/external-link-J-x6ty1h.js +6 -0
- package/packages/remote-control-server/web/dist/assets/{flowDiagram-I6XJVG4X-BSuXctT4.js → flowDiagram-I6XJVG4X-Ba7lo6ds.js} +1 -1
- package/{dist/rcs-web/assets/ganttDiagram-6RSMTGT7-B87oxjmY.js → packages/remote-control-server/web/dist/assets/ganttDiagram-6RSMTGT7-CKkj67eu.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{gitGraphDiagram-PVQCEYII-BRrwOi3C.js → gitGraphDiagram-PVQCEYII-205YdSRe.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{infoDiagram-5YYISTIA-B-8QXS0n.js → infoDiagram-5YYISTIA-CpPvGigJ.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{ishikawaDiagram-YF4QCWOH-Bau9MU6C.js → ishikawaDiagram-YF4QCWOH-D0ZEWQy7.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{journeyDiagram-JHISSGLW-D_tAp--R.js → journeyDiagram-JHISSGLW-DVl0M9a7.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{kanban-definition-UN3LZRKU---UIvjMZ.js → kanban-definition-UN3LZRKU-Dkr8uJC-.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{linear-CIDLjd5h.js → linear-CIdvseqY.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/main-B8hkVvfg.js +265 -0
- package/packages/remote-control-server/web/dist/assets/main-C2O-g177.css +1 -0
- package/packages/remote-control-server/web/dist/assets/{mermaid-VLURNSYL-Cu5A5sMk.js → mermaid-VLURNSYL-DdTSeoZS.js} +4 -4
- package/packages/remote-control-server/web/dist/assets/{mermaid.core-CEJOtCGw.js → mermaid.core-CtvzCSq2.js} +5 -5
- package/{dist/rcs-web/assets/mindmap-definition-RKZ34NQL-ldyccyCP.js → packages/remote-control-server/web/dist/assets/mindmap-definition-RKZ34NQL-CMlDCaV_.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{pieDiagram-4H26LBE5-CmghofeF.js → pieDiagram-4H26LBE5-C6xMizTL.js} +1 -1
- package/{dist/rcs-web/assets/quadrantDiagram-W4KKPZXB-B-2N2UHr.js → packages/remote-control-server/web/dist/assets/quadrantDiagram-W4KKPZXB-D6epraE1.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{requirementDiagram-4Y6WPE33-39jlO9Y8.js → requirementDiagram-4Y6WPE33-BHmML6bk.js} +1 -1
- package/{dist/rcs-web/assets/sankeyDiagram-5OEKKPKP-DdY7KNFB.js → packages/remote-control-server/web/dist/assets/sankeyDiagram-5OEKKPKP-CIKij54Y.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{sequenceDiagram-3UESZ5HK-D2k5rX6x.js → sequenceDiagram-3UESZ5HK-CY0y5zdR.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{stateDiagram-AJRCARHV-CP9N457o.js → stateDiagram-AJRCARHV-FBhNz1Lc.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{stateDiagram-v2-BHNVJYJU-CPhZYlL0.js → stateDiagram-v2-BHNVJYJU-m7ruBTu5.js} +1 -1
- package/{dist/rcs-web/assets/timeline-definition-PNZ67QCA-DSEPBO8O.js → packages/remote-control-server/web/dist/assets/timeline-definition-PNZ67QCA-CJopHs1x.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{vennDiagram-CIIHVFJN-CkNMwCJ3.js → vennDiagram-CIIHVFJN-DF_uHOP7.js} +1 -1
- package/packages/remote-control-server/web/dist/assets/{wardleyDiagram-YWT4CUSO-Cxd-F60s.js → wardleyDiagram-YWT4CUSO-DQmjIZJA.js} +1 -1
- package/{dist/rcs-web/assets/xychartDiagram-2RQKCTM6-BLL105NN.js → packages/remote-control-server/web/dist/assets/xychartDiagram-2RQKCTM6-v8zL-j4G.js} +1 -1
- package/packages/remote-control-server/web/dist/index.html +2 -2
- package/dist/rcs-web/assets/SessionDetail-D-xhkyLo.js +0 -65
- package/dist/rcs-web/assets/SessionVisibilityBadge-91WyR9K6.js +0 -11
- package/dist/rcs-web/assets/ShareLanding-DhwgJb6V.js +0 -1
- package/dist/rcs-web/assets/channel-CCfqpGSl.js +0 -1
- package/dist/rcs-web/assets/classDiagram-4FO5ZUOK-DzNdVs3v.js +0 -1
- package/dist/rcs-web/assets/classDiagram-v2-Q7XG4LA2-DzNdVs3v.js +0 -1
- package/dist/rcs-web/assets/main-DhbquBXW.css +0 -1
- package/dist/rcs-web/assets/main-N3GYCdLF.js +0 -260
- package/packages/remote-control-server/web/dist/assets/SessionDetail-D-xhkyLo.js +0 -65
- package/packages/remote-control-server/web/dist/assets/SessionVisibilityBadge-91WyR9K6.js +0 -11
- package/packages/remote-control-server/web/dist/assets/ShareLanding-DhwgJb6V.js +0 -1
- package/packages/remote-control-server/web/dist/assets/channel-CCfqpGSl.js +0 -1
- package/packages/remote-control-server/web/dist/assets/classDiagram-4FO5ZUOK-DzNdVs3v.js +0 -1
- package/packages/remote-control-server/web/dist/assets/classDiagram-v2-Q7XG4LA2-DzNdVs3v.js +0 -1
- package/packages/remote-control-server/web/dist/assets/main-DhbquBXW.css +0 -1
- 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')
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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 (
|
|
303
|
-
// the env owner (the wait-mode worker polls under the same
|
|
304
|
-
// registered with) and admins. Unowned envs (claim pending)
|
|
305
|
-
// nothing is bound yet, so there is nothing to disclose or keep
|
|
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
|
|
349
|
-
//
|
|
350
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|