@cyberismo/backend 1.1.1 → 1.2.0

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 (305) hide show
  1. package/README.md +21 -0
  2. package/dist/app.js +22 -11
  3. package/dist/app.js.map +1 -1
  4. package/dist/auth/index.d.ts +1 -1
  5. package/dist/auth/index.js +1 -1
  6. package/dist/auth/index.js.map +1 -1
  7. package/dist/auth/keycloak.d.ts +2 -3
  8. package/dist/auth/keycloak.js +6 -0
  9. package/dist/auth/keycloak.js.map +1 -1
  10. package/dist/auth/mock.d.ts +12 -8
  11. package/dist/auth/mock.js +119 -31
  12. package/dist/auth/mock.js.map +1 -1
  13. package/dist/auth/types.d.ts +7 -1
  14. package/dist/domain/cards/index.js +11 -84
  15. package/dist/domain/cards/index.js.map +1 -1
  16. package/dist/domain/cards/lib.js +2 -11
  17. package/dist/domain/cards/lib.js.map +1 -1
  18. package/dist/domain/cards/schema.js +9 -3
  19. package/dist/domain/cards/schema.js.map +1 -1
  20. package/dist/domain/cards/service.d.ts +2 -5
  21. package/dist/domain/cards/service.js +8 -15
  22. package/dist/domain/cards/service.js.map +1 -1
  23. package/dist/domain/events/index.d.ts +15 -0
  24. package/dist/domain/events/index.js +94 -0
  25. package/dist/domain/events/index.js.map +1 -0
  26. package/dist/domain/events/lifetime.d.ts +14 -0
  27. package/dist/domain/events/lifetime.js +25 -0
  28. package/dist/domain/events/lifetime.js.map +1 -0
  29. package/dist/domain/events/project-events.d.ts +67 -0
  30. package/dist/domain/events/project-events.js +200 -0
  31. package/dist/domain/events/project-events.js.map +1 -0
  32. package/dist/domain/events/schema.d.ts +22 -0
  33. package/dist/domain/events/schema.js +20 -0
  34. package/dist/domain/events/schema.js.map +1 -0
  35. package/dist/domain/project/index.js +17 -5
  36. package/dist/domain/project/index.js.map +1 -1
  37. package/dist/domain/project/schema.d.ts +8 -0
  38. package/dist/domain/project/schema.js +34 -6
  39. package/dist/domain/project/schema.js.map +1 -1
  40. package/dist/domain/project/service.d.ts +9 -5
  41. package/dist/domain/project/service.js +22 -16
  42. package/dist/domain/project/service.js.map +1 -1
  43. package/dist/domain/projects/index.js +5 -1
  44. package/dist/domain/projects/index.js.map +1 -1
  45. package/dist/domain/resources/index.js +74 -2
  46. package/dist/domain/resources/index.js.map +1 -1
  47. package/dist/domain/resources/schema.d.ts +8 -2
  48. package/dist/domain/resources/schema.js +6 -1
  49. package/dist/domain/resources/schema.js.map +1 -1
  50. package/dist/domain/resources/service.d.ts +13 -1
  51. package/dist/domain/resources/service.js +45 -2
  52. package/dist/domain/resources/service.js.map +1 -1
  53. package/dist/domain/skills/index.d.ts +15 -0
  54. package/dist/domain/skills/index.js +52 -0
  55. package/dist/domain/skills/index.js.map +1 -0
  56. package/dist/domain/skills/schema.d.ts +16 -0
  57. package/dist/domain/skills/schema.js +18 -0
  58. package/dist/domain/skills/schema.js.map +1 -0
  59. package/dist/domain/skills/service.d.ts +14 -0
  60. package/dist/domain/skills/service.js +16 -0
  61. package/dist/domain/skills/service.js.map +1 -0
  62. package/dist/export.d.ts +3 -2
  63. package/dist/export.js +53 -38
  64. package/dist/export.js.map +1 -1
  65. package/dist/index.d.ts +1 -0
  66. package/dist/index.js +3 -0
  67. package/dist/index.js.map +1 -1
  68. package/dist/main.js +23 -10
  69. package/dist/main.js.map +1 -1
  70. package/dist/middleware/auth.d.ts +5 -0
  71. package/dist/middleware/auth.js +20 -4
  72. package/dist/middleware/auth.js.map +1 -1
  73. package/dist/middleware/commandManager.d.ts +2 -0
  74. package/dist/middleware/commandManager.js +1 -0
  75. package/dist/middleware/commandManager.js.map +1 -1
  76. package/dist/overlay.d.ts +45 -0
  77. package/dist/overlay.js +157 -0
  78. package/dist/overlay.js.map +1 -0
  79. package/dist/project-errors.d.ts +18 -0
  80. package/dist/project-errors.js +25 -0
  81. package/dist/project-errors.js.map +1 -0
  82. package/dist/project-registry.d.ts +9 -1
  83. package/dist/project-registry.js +37 -9
  84. package/dist/project-registry.js.map +1 -1
  85. package/dist/public/THIRD-PARTY.txt +528 -435
  86. package/dist/public/assets/__vite-browser-external-PaJ7c2l8.js +1 -0
  87. package/dist/public/assets/abnfDiagram-VCTEODGH-DB2dfOGe.js +1 -0
  88. package/dist/public/assets/architecture-7GRP2DOG-2V0X8hxt.js +1 -0
  89. package/dist/public/assets/architectureDiagram-5GKGNRK7-CRhUR2Ul.js +36 -0
  90. package/dist/public/assets/blockDiagram-I7D4REHJ-DJKsYF1v.js +129 -0
  91. package/dist/public/assets/c4Diagram-7LVT6UL2-BTwMzoAF.js +38 -0
  92. package/dist/public/assets/channel-C0JtPpMA.js +1 -0
  93. package/dist/public/assets/chunk-4HAMMTFA-DjjgrBsB.js +62 -0
  94. package/dist/public/assets/chunk-75Z2AOVW-CKqXRGJV.js +2 -0
  95. package/dist/public/assets/chunk-DU6HZSFF-iJGdiRt7.js +127 -0
  96. package/dist/public/assets/chunk-F27PBJKO-tDfs6zeO.js +1 -0
  97. package/dist/public/assets/chunk-FOHPRMQF-DzxwRta7.js +161 -0
  98. package/dist/public/assets/chunk-GMAD6QVW-DxHVEaGj.js +72 -0
  99. package/dist/public/assets/chunk-GVQU2GXP-AMxD4_Ta.js +1 -0
  100. package/dist/public/assets/{chunk-EX3LRPZG-Db0XYyxy.js → chunk-IMKFNOWR-0rz8QrYC.js} +2 -2
  101. package/dist/public/assets/chunk-L3NEJ4N5-BK3KAKMe.js +1 -0
  102. package/dist/public/assets/chunk-OSK3NFVY-B8MdrI_M.js +10 -0
  103. package/dist/public/assets/chunk-P2QGCYS3-D3J5AcHg.js +1 -0
  104. package/dist/public/assets/{chunk-VR4S4FIN-tUhbELKr.js → chunk-POPQ4Y6H-CsYg_XkK.js} +1 -1
  105. package/dist/public/assets/{chunk-HOUHSVGY-L35GPy9G.js → chunk-PWAF6VOD-D9cdEIZp.js} +1 -1
  106. package/dist/public/assets/chunk-SHT3W25Y-1jjokCf3.js +168 -0
  107. package/dist/public/assets/{chunk-MOJQB5TN-D1shKbMs.js → chunk-SVP7TREG-CIQMl7hl.js} +1 -1
  108. package/dist/public/assets/{chunk-V7JOEXUC-71b0ABpv.js → chunk-TICWLB2K-Ccsy2PYz.js} +3 -3
  109. package/dist/public/assets/chunk-XXDRQBXY-B7bWyHjt.js +1 -0
  110. package/dist/public/assets/classDiagram-ZZMXUADV-mWyXNAXi.js +1 -0
  111. package/dist/public/assets/classDiagram-v2-VYDZK3BY-mWyXNAXi.js +1 -0
  112. package/dist/public/assets/cose-bilkent-JH36ORCC-DPzc3Pv4.js +1 -0
  113. package/dist/public/assets/cynefin-OW5HDTMX-BIVDCYOU.js +1 -0
  114. package/dist/public/assets/cynefinDiagram-5FMLGOSQ-_V_nrIhD.js +62 -0
  115. package/dist/public/assets/cytoscape.esm-NgwPT4ez.js +321 -0
  116. package/dist/public/assets/dagre-C-rnJpZF.js +1 -0
  117. package/dist/public/assets/dagre-GXQ25YYZ-CJc0wc6f.js +4 -0
  118. package/dist/public/assets/diagram-S7CK7UJ4-25IjIKyo.js +30 -0
  119. package/dist/public/assets/diagram-UQ7AKVKN-C5VvsV7x.js +41 -0
  120. package/dist/public/assets/{diagram-FQU43EPY-DS-ryuRg.js → diagram-VSXAHHWV-Dw3rKVL_.js} +3 -3
  121. package/dist/public/assets/diagram-VX7I27RA-De_UQv8C.js +24 -0
  122. package/dist/public/assets/diagram-Z3DM3KII-Bd-65QQW.js +24 -0
  123. package/dist/public/assets/dist-BDCdLWk4.js +1 -0
  124. package/dist/public/assets/ebnfDiagram-PWID7BFC-CrxdET5U.js +1 -0
  125. package/dist/public/assets/erDiagram-RLTQ6QDP-BKo0Uyna.js +99 -0
  126. package/dist/public/assets/eventmodeling-NTZA5JFV-DKkAdJZd.js +1 -0
  127. package/dist/public/assets/flowDiagram-HODETNUW-CEE2wjdt.js +1 -0
  128. package/dist/public/assets/ganttDiagram-EL5Y4UJY-BHFn8vk0.js +292 -0
  129. package/dist/public/assets/gitGraph-4MIJSDKK-Bpvnb45u.js +1 -0
  130. package/dist/public/assets/{gitGraphDiagram-IHSO6WYX-CksvJvsQ.js → gitGraphDiagram-WWUBYQGX-Bw0EoxBY.js} +9 -9
  131. package/dist/public/assets/index-D3HS7YxF.js +2228 -0
  132. package/dist/public/assets/index-D6WqGtsG.js +27876 -0
  133. package/dist/public/assets/{index-COSSLZqt.css → index-Sv3WxZ_8.css} +1 -1
  134. package/dist/public/assets/info-A6RAGUB7-BvvG_zrk.js +1 -0
  135. package/dist/public/assets/infoDiagram-27XIBGKW-CtpdJmtL.js +2 -0
  136. package/dist/public/assets/{ishikawaDiagram-FXEZZL3T-CiLpASIW.js → ishikawaDiagram-5VMMS53U-KvT4DU1u.js} +2 -2
  137. package/dist/public/assets/journeyDiagram-3NMN7TZE-PhGRkXkq.js +139 -0
  138. package/dist/public/assets/{kanban-definition-HUTT4EX6-DYP_x6zD.js → kanban-definition-UXKFOSKX-Oyx5QgRq.js} +4 -4
  139. package/dist/public/assets/{katex-DolUETbr.js → katex-ZlcWpGUi.js} +3 -3
  140. package/dist/public/assets/{line-C8Sfsgt8.js → line-Dr41HTk1.js} +1 -1
  141. package/dist/public/assets/linear-DDBi9FwZ.js +1 -0
  142. package/dist/public/assets/mermaid-parser.core-DsIAxDBa.js +7 -0
  143. package/dist/public/assets/{mindmap-definition-LN4V7U3C-3IAd4gp3.js → mindmap-definition-YA3MSWOX-r8hvd6gP.js} +1 -1
  144. package/dist/public/assets/packet-AYTQ26CC-B0EpJ2Sc.js +1 -0
  145. package/dist/public/assets/path-B_0haWDX.js +1 -0
  146. package/dist/public/assets/pegDiagram-XKGWAZYB-DaNkQTQF.js +1 -0
  147. package/dist/public/assets/pie-WAS4IAKB-Dt2zttL9.js +1 -0
  148. package/dist/public/assets/pieDiagram-E7YTZNPT-x0XWEGbd.js +39 -0
  149. package/dist/public/assets/{quadrantDiagram-ABIIQ3AL-B3czMbdS.js → quadrantDiagram-AXDQQJYC-B9dlNtZl.js} +3 -3
  150. package/dist/public/assets/radar-RG4KPBEZ-DjbR4-5l.js +1 -0
  151. package/dist/public/assets/railroad-74A4TZTK-Bk3B5r_R.js +1 -0
  152. package/dist/public/assets/railroad-abnf-HS5TGJTU-DxaWfACD.js +1 -0
  153. package/dist/public/assets/railroad-ebnf-LZEXJU2U-DscxXZO0.js +1 -0
  154. package/dist/public/assets/railroad-peg-WCYAUIDC-BCWzRFzw.js +1 -0
  155. package/dist/public/assets/railroadDiagram-O6MQD6OU-DhwsoX6A.js +1 -0
  156. package/dist/public/assets/{requirementDiagram-TGXJPOKE-CmvJWy_S.js → requirementDiagram-BXWQKSXE-BiqZ6axf.js} +2 -2
  157. package/dist/public/assets/rolldown-runtime-B0Z9INg1.js +1 -0
  158. package/dist/public/assets/{rough.esm-By172zw-.js → rough.esm-Dy-Kn_BL.js} +1 -1
  159. package/dist/public/assets/{sankeyDiagram-HTMAVEWB-tAeOzMTn.js → sankeyDiagram-P5KCCOFB-C4rb2pZ9.js} +1 -1
  160. package/dist/public/assets/{sequenceDiagram-DBY2YBRQ-r0pwijM4.js → sequenceDiagram-WJ2MYXX4-DRKTnGD7.js} +9 -9
  161. package/dist/public/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
  162. package/dist/public/assets/src-an_qrnIj.js +1 -0
  163. package/dist/public/assets/stateDiagram-D77RDMKH-xxdybg5Z.js +1 -0
  164. package/dist/public/assets/stateDiagram-v2-MP3YSRHH-CXzmJy_a.js +1 -0
  165. package/dist/public/assets/swimlanes-42K2YHIH-DyGI1gI6.js +1 -0
  166. package/dist/public/assets/swimlanesDiagram-VR7AAH4N-7hqb6VoW.js +8 -0
  167. package/dist/public/assets/timeline-definition-24CTP7MA-C2pIp77Q.js +120 -0
  168. package/dist/public/assets/treeView-Q6P3EWNA-CxRtaaB8.js +1 -0
  169. package/dist/public/assets/treemap-WGGIJYW6-G1IHiwTa.js +1 -0
  170. package/dist/public/assets/{vennDiagram-L72KCM5P-ti2qNcvo.js → vennDiagram-4TSXK5OY-CwimQRR9.js} +2 -2
  171. package/dist/public/assets/wardley-WFR3VGLG-DkQ3TjZX.js +1 -0
  172. package/dist/public/assets/{wardleyDiagram-EHGQE667-C8lblm8x.js → wardleyDiagram-VM6X3IG4-BOXpwLCv.js} +15 -15
  173. package/dist/public/assets/{xychartDiagram-FW5EYKEG-US-_k5eu.js → xychartDiagram-S5SC5T6Z-ChOPYsHc.js} +3 -3
  174. package/dist/public/index.html +19 -26
  175. package/dist/public/sbom.cdx.json +1 -0
  176. package/dist/utils.d.ts +4 -0
  177. package/dist/utils.js +6 -0
  178. package/dist/utils.js.map +1 -1
  179. package/package.json +12 -13
  180. package/src/app.ts +22 -11
  181. package/src/auth/index.ts +1 -1
  182. package/src/auth/keycloak.ts +8 -3
  183. package/src/auth/mock.ts +141 -35
  184. package/src/auth/types.ts +8 -1
  185. package/src/domain/cards/index.ts +12 -102
  186. package/src/domain/cards/lib.ts +2 -14
  187. package/src/domain/cards/schema.ts +11 -3
  188. package/src/domain/cards/service.ts +9 -24
  189. package/src/domain/events/index.ts +112 -0
  190. package/src/domain/events/lifetime.ts +27 -0
  191. package/src/domain/events/project-events.ts +254 -0
  192. package/src/domain/events/schema.ts +21 -0
  193. package/src/domain/project/index.ts +31 -3
  194. package/src/domain/project/schema.ts +39 -6
  195. package/src/domain/project/service.ts +42 -17
  196. package/src/domain/projects/index.ts +5 -1
  197. package/src/domain/resources/index.ts +85 -2
  198. package/src/domain/resources/schema.ts +12 -2
  199. package/src/domain/resources/service.ts +55 -3
  200. package/src/domain/skills/index.ts +61 -0
  201. package/src/domain/skills/schema.ts +19 -0
  202. package/src/domain/skills/service.ts +18 -0
  203. package/src/export.ts +60 -42
  204. package/src/index.ts +3 -0
  205. package/src/main.ts +30 -11
  206. package/src/middleware/auth.ts +23 -4
  207. package/src/middleware/commandManager.ts +3 -0
  208. package/src/overlay.ts +175 -0
  209. package/src/project-errors.ts +33 -0
  210. package/src/project-registry.ts +41 -10
  211. package/src/utils.ts +10 -0
  212. package/dist/domain/cards/presence.d.ts +0 -50
  213. package/dist/domain/cards/presence.js +0 -93
  214. package/dist/domain/cards/presence.js.map +0 -1
  215. package/dist/public/assets/abnfDiagram-VRR7QNED-Djr4KJ5E.js +0 -1
  216. package/dist/public/assets/arc-DrR4k_H-.js +0 -1
  217. package/dist/public/assets/architecture-TIHT7OUA-XL6N-4o9.js +0 -1
  218. package/dist/public/assets/architectureDiagram-ZJ3FMSHR-DsMU2Uwa.js +0 -36
  219. package/dist/public/assets/blockDiagram-677ZJIJ3-DRBmQUQJ.js +0 -132
  220. package/dist/public/assets/c4Diagram-LMCZKHZV-E0rM02LD.js +0 -10
  221. package/dist/public/assets/channel-D5fnImJy.js +0 -1
  222. package/dist/public/assets/chunk-32BRIVSS-BnDY0aSV.js +0 -1
  223. package/dist/public/assets/chunk-52WLFC77-N5YugKaZ.js +0 -10
  224. package/dist/public/assets/chunk-5FCAYU7R-I3_t_Ic_.js +0 -1
  225. package/dist/public/assets/chunk-5HE753X5-Dxz40rzH.js +0 -1
  226. package/dist/public/assets/chunk-5JV3BV7I-CNwCKRLH.js +0 -1
  227. package/dist/public/assets/chunk-5TONJI2A-ezWQ_Z2j.js +0 -2
  228. package/dist/public/assets/chunk-7BUUIJ7U-Bb538aSH.js +0 -1
  229. package/dist/public/assets/chunk-BIQX33UG-DS132-R_.js +0 -1
  230. package/dist/public/assets/chunk-C7G6YPKG-BN-cNjoj.js +0 -1
  231. package/dist/public/assets/chunk-CQNSW5MT-D8Mn1I73.js +0 -1
  232. package/dist/public/assets/chunk-CYSBUYHQ-Dc64Dwdo.js +0 -1
  233. package/dist/public/assets/chunk-EMLP6XTP-aIK0V8GX.js +0 -1
  234. package/dist/public/assets/chunk-FWX5IMBZ-BF_P2-Do.js +0 -2
  235. package/dist/public/assets/chunk-ICXQ74PX-Bb8zaHde.js +0 -2
  236. package/dist/public/assets/chunk-JG7HCLWE-BtAXK3iQ.js +0 -2
  237. package/dist/public/assets/chunk-KEIR6QF5-DEvma7oi.js +0 -161
  238. package/dist/public/assets/chunk-MOZMSUNE-5FIqIBsY.js +0 -1
  239. package/dist/public/assets/chunk-OGEWGWER-BVIFy9xz.js +0 -1
  240. package/dist/public/assets/chunk-OSBZ3O6U-Cfd7kf1R.js +0 -1
  241. package/dist/public/assets/chunk-PUDLZKDR-Db3FSNR9.js +0 -156
  242. package/dist/public/assets/chunk-Q4XR5HBZ-BuKFRUzR.js +0 -70
  243. package/dist/public/assets/chunk-QBLGF6JB-DUGZXFh-.js +0 -1
  244. package/dist/public/assets/chunk-R7FJI6CG-D7mUd-qN.js +0 -1
  245. package/dist/public/assets/chunk-RYQCIY6F-C_3rESBd.js +0 -1
  246. package/dist/public/assets/chunk-U6XO7XAA-C3UhlwA1.js +0 -2
  247. package/dist/public/assets/chunk-VAUOI2AC-C-GBw18h.js +0 -1
  248. package/dist/public/assets/chunk-WYO6CB5R-BQoO62-B.js +0 -127
  249. package/dist/public/assets/chunk-XXDRQBXY-Co8n4ZHT.js +0 -1
  250. package/dist/public/assets/chunk-YOTPTUD7-DhCRKU1A.js +0 -1
  251. package/dist/public/assets/chunk-ZGVPDNZ5-hpz2Ryq5.js +0 -62
  252. package/dist/public/assets/chunk-ZIRB5QZD-BpeS1aZ7.js +0 -32
  253. package/dist/public/assets/classDiagram-OUVF2IWQ-CXdVorPK.js +0 -1
  254. package/dist/public/assets/classDiagram-v2-EOCWNBFH-CXdVorPK.js +0 -1
  255. package/dist/public/assets/colors-Cg5Vh3br.js +0 -1
  256. package/dist/public/assets/cose-bilkent-JH36ORCC-4GoRbloS.js +0 -1
  257. package/dist/public/assets/cynefin-VYW2F7L2-BPH-25fI.js +0 -1
  258. package/dist/public/assets/cynefinDiagram-TSTJHNR4-PEf16euv.js +0 -62
  259. package/dist/public/assets/cytoscape.esm-C3UvhD5t.js +0 -321
  260. package/dist/public/assets/dagre-Buvkdvvj.js +0 -1
  261. package/dist/public/assets/dagre-VKFMJZFB-DPsCMBF1.js +0 -4
  262. package/dist/public/assets/diagram-G47NLZAW-CzUoYkFf.js +0 -24
  263. package/dist/public/assets/diagram-NH7WQ7WH-MhGnE2fh.js +0 -24
  264. package/dist/public/assets/diagram-OA4YK3LP-CsQ9aFR_.js +0 -30
  265. package/dist/public/assets/diagram-WEI45ONY-Er3q49ew.js +0 -41
  266. package/dist/public/assets/dist-BzwexiH1.js +0 -1
  267. package/dist/public/assets/ebnfDiagram-CCIWWBDH-AnJAsk14.js +0 -1
  268. package/dist/public/assets/erDiagram-Q63AITRT-DulA16my.js +0 -85
  269. package/dist/public/assets/eventmodeling-45OFAUF4-CoIhTpHU.js +0 -1
  270. package/dist/public/assets/flowDiagram-23GEKE2U-BGMXUq18.js +0 -1
  271. package/dist/public/assets/ganttDiagram-NO4QXBWP-DAvCPT7_.js +0 -292
  272. package/dist/public/assets/gitGraph-TEB2WS4Q-BcPsZts5.js +0 -1
  273. package/dist/public/assets/index-H1CkT-JT.js +0 -447
  274. package/dist/public/assets/info-DKCQHKI2-7VGzrAVB.js +0 -1
  275. package/dist/public/assets/infoDiagram-FWYZ7A6U-D_by7lSL.js +0 -2
  276. package/dist/public/assets/journeyDiagram-5HDEW3XC-DpvFmk7c.js +0 -139
  277. package/dist/public/assets/linear-BWTvbP1l.js +0 -1
  278. package/dist/public/assets/map-BaFkSB1l.js +0 -1
  279. package/dist/public/assets/mermaid-parser.core--8614tkK.js +0 -4
  280. package/dist/public/assets/ordinal-BdBDtYTt.js +0 -1
  281. package/dist/public/assets/packet-7NZHBO7P-KYt33H0C.js +0 -1
  282. package/dist/public/assets/path-HTXttWil.js +0 -1
  283. package/dist/public/assets/pegDiagram-2B236MQR-BQNFxYz2.js +0 -1
  284. package/dist/public/assets/pie-RZYD4A2V-xKz9s9I4.js +0 -1
  285. package/dist/public/assets/pieDiagram-ENE6RG2P-Cy13tg8_.js +0 -39
  286. package/dist/public/assets/radar-I7S5WNFK-B4QTX-fX.js +0 -1
  287. package/dist/public/assets/railroad-3IZDKUUU-Co_9rPVC.js +0 -1
  288. package/dist/public/assets/railroad-abnf-AHOZXSZD-Bhy_c3ol.js +0 -1
  289. package/dist/public/assets/railroad-ebnf-EBAXGLYW-B9ENjG34.js +0 -1
  290. package/dist/public/assets/railroad-peg-LSFZ7HO6-CyJxDBsB.js +0 -1
  291. package/dist/public/assets/railroadDiagram-RFXS5EU6-M2j5SJF2.js +0 -1
  292. package/dist/public/assets/range-EplFIxc7.js +0 -1
  293. package/dist/public/assets/sizeCapture-X5ZJPWSS-B0uUizjq.js +0 -1
  294. package/dist/public/assets/src-DOh7_en_.js +0 -1
  295. package/dist/public/assets/stateDiagram-2N3HPSRC-a9IdkSQG.js +0 -1
  296. package/dist/public/assets/stateDiagram-v2-6OUMAXLB-s2blPtZ-.js +0 -1
  297. package/dist/public/assets/swimlanes-5IMT3BWC-5w0Cdxk6.js +0 -2
  298. package/dist/public/assets/swimlanesDiagram-G3AALYLV-BWa-KRed.js +0 -8
  299. package/dist/public/assets/time-BM2d9MsQ.js +0 -1
  300. package/dist/public/assets/timeline-definition-FHXFAJF6-CvXlv_4Y.js +0 -120
  301. package/dist/public/assets/treeView-QDETBFTQ-B9cxjfpB.js +0 -1
  302. package/dist/public/assets/treemap-6X3UGDF4-BplEq4hB.js +0 -1
  303. package/dist/public/assets/treemap-BfjIY-m6.js +0 -1
  304. package/dist/public/assets/wardley-OPB4EBWU-BX2ghg5e.js +0 -1
  305. package/src/domain/cards/presence.ts +0 -124
package/src/auth/mock.ts CHANGED
@@ -11,30 +11,103 @@
11
11
  License along with this program. If not, see <https://www.gnu.org/licenses/>.
12
12
  */
13
13
 
14
- import type { MiddlewareHandler } from 'hono';
14
+ import type { Context, MiddlewareHandler } from 'hono';
15
15
  import { setCookie } from 'hono/cookie';
16
16
  import { UserRole } from '../types.js';
17
17
  import type { UserInfo } from '../types.js';
18
- import type { AuthProvider } from './types.js';
18
+ import type { AuthProvider, AuthResult } from './types.js';
19
19
 
20
20
  export interface MockUserConfig {
21
21
  name?: string;
22
22
  email?: string;
23
+ roster?: boolean;
23
24
  }
24
25
 
25
26
  export const MOCK_ROLE_COOKIE = 'mock-role';
26
- const ROLE_RESET_VALUE = 'default';
27
+ export const MOCK_USER_COOKIE = 'mock-user';
28
+ export const MOCK_EXP_COOKIE = 'mock-exp';
29
+ export const MOCK_AS_PARAM = 'as';
30
+ const RESET_VALUE = 'default';
31
+ const MAX_TTL_SECONDS = 86_400;
32
+ // Mock sessions do not expire. Saying so with a distant deadline rather than
33
+ // no deadline keeps `exp` total for every consumer: it is far beyond the
34
+ // 30-minute stream lifetime, so computeLifetimeMs() min()s it away and
35
+ // rotation still falls to the jitter, exactly as an absent one did.
36
+ const NO_EXPIRY_SECONDS = 100 * 365 * 24 * 60 * 60;
27
37
 
28
- const ROLE_ALIASES: Record<string, UserRole> = {
29
- reader: UserRole.Reader,
30
- editor: UserRole.Editor,
31
- admin: UserRole.Admin,
32
- connector: UserRole.Connector,
33
- };
38
+ function noExpiry(): number {
39
+ return Math.floor(Date.now() / 1000) + NO_EXPIRY_SECONDS;
40
+ }
41
+
42
+ const ROLE_ALIASES = new Map<string, UserRole>([
43
+ ['reader', UserRole.Reader],
44
+ ['editor', UserRole.Editor],
45
+ ['admin', UserRole.Admin],
46
+ ['connector', UserRole.Connector],
47
+ ]);
48
+
49
+ interface MockUser {
50
+ id: string;
51
+ name: string;
52
+ email: string;
53
+ role: UserRole;
54
+ }
55
+
56
+ // Opt-in: `cyberismo app` ships mock auth, where a roster identity would forge
57
+ // git commit authorship. Dev and e2e need several users in one backend process.
58
+ const MOCK_USERS = new Map<string, MockUser>([
59
+ [
60
+ 'alice',
61
+ {
62
+ id: 'mock-user-alice',
63
+ name: 'Alice',
64
+ email: 'alice@example.com',
65
+ role: UserRole.Admin,
66
+ },
67
+ ],
68
+ [
69
+ 'bob',
70
+ {
71
+ id: 'mock-user-bob',
72
+ name: 'Bob',
73
+ email: 'bob@example.com',
74
+ role: UserRole.Editor,
75
+ },
76
+ ],
77
+ [
78
+ 'carol',
79
+ {
80
+ id: 'mock-user-carol',
81
+ name: 'Carol',
82
+ email: 'carol@example.com',
83
+ role: UserRole.Reader,
84
+ },
85
+ ],
86
+ ]);
34
87
 
35
88
  function parseRole(value: string | null | undefined): UserRole | null {
36
89
  if (!value) return null;
37
- return ROLE_ALIASES[value.toLowerCase()] ?? null;
90
+ return ROLE_ALIASES.get(value.toLowerCase()) ?? null;
91
+ }
92
+
93
+ function parseUser(value: string | null | undefined): MockUser | null {
94
+ if (!value) return null;
95
+ return MOCK_USERS.get(value.toLowerCase()) ?? null;
96
+ }
97
+
98
+ /** An unreadable deadline is dropped rather than treated as long past, which
99
+ * would lock the user out with no request left to clear the cookie. */
100
+ function parseExpiry(value: string | null | undefined): number | null {
101
+ const exp = Number(value);
102
+ return value && Number.isInteger(exp) && exp > 0 ? exp : null;
103
+ }
104
+
105
+ /** Seconds from now to the absolute epoch-second deadline to store. */
106
+ function toDeadline(value: string): string | null {
107
+ const seconds = Number(value);
108
+ if (!Number.isInteger(seconds) || seconds <= 0) return null;
109
+ if (seconds > MAX_TTL_SECONDS) return null;
110
+ return String(Math.floor(Date.now() / 1000) + seconds);
38
111
  }
39
112
 
40
113
  function readCookie(header: string | null, name: string): string | undefined {
@@ -48,6 +121,22 @@ function readCookie(header: string | null, name: string): string | undefined {
48
121
  return undefined;
49
122
  }
50
123
 
124
+ function applyOverride(
125
+ c: Context,
126
+ override: string | null,
127
+ cookieName: string,
128
+ toCookieValue: (value: string) => string | null,
129
+ ): void {
130
+ if (!override) return;
131
+ if (override.toLowerCase() === RESET_VALUE) {
132
+ setCookie(c, cookieName, '', { path: '/', maxAge: 0 });
133
+ return;
134
+ }
135
+ const value = toCookieValue(override);
136
+ if (value === null) return;
137
+ setCookie(c, cookieName, value, { path: '/', sameSite: 'Lax' });
138
+ }
139
+
51
140
  export class MockAuthProvider implements AuthProvider {
52
141
  private readonly userConfig: MockUserConfig;
53
142
 
@@ -55,36 +144,53 @@ export class MockAuthProvider implements AuthProvider {
55
144
  this.userConfig = config ?? {};
56
145
  }
57
146
 
58
- async authenticate(req: Request): Promise<UserInfo> {
59
- const cookieRole = parseRole(
60
- readCookie(req.headers.get('cookie'), MOCK_ROLE_COOKIE),
61
- );
62
- return {
147
+ async authenticate(req: Request): Promise<AuthResult | null> {
148
+ const cookies = req.headers.get('cookie');
149
+ const cookieRole = parseRole(readCookie(cookies, MOCK_ROLE_COOKIE));
150
+ if (this.userConfig.roster) {
151
+ // One-shot: a second identity from inside a tab already signed in as
152
+ // someone else, so neither the cookie nor the role override applies.
153
+ const asUser = parseUser(
154
+ new URL(req.url).searchParams.get(MOCK_AS_PARAM),
155
+ );
156
+ if (asUser) return { ...asUser, exp: noExpiry() };
157
+ }
158
+ const rosterUser = this.userConfig.roster
159
+ ? parseUser(readCookie(cookies, MOCK_USER_COOKIE))
160
+ : null;
161
+ const expiry = this.userConfig.roster
162
+ ? parseExpiry(readCookie(cookies, MOCK_EXP_COOKIE))
163
+ : null;
164
+ if (expiry !== null && expiry * 1000 <= Date.now()) return null;
165
+ const exp = expiry ?? noExpiry();
166
+
167
+ const identity: UserInfo = rosterUser ?? {
63
168
  id: 'mock-user',
64
169
  email: this.userConfig.email ?? 'admin@cyberismo.local',
65
170
  name: this.userConfig.name ?? 'Local Admin',
66
- role: cookieRole ?? UserRole.Admin,
171
+ role: UserRole.Admin,
67
172
  };
173
+ return { ...identity, role: cookieRole ?? identity.role, exp };
68
174
  }
69
- }
70
175
 
71
- /**
72
- * Dev-only middleware that turns `?role=<reader|editor|admin>` into a persistent
73
- * `mock-role` cookie, and clears it on `?role=default`.
74
- */
75
- export function mockRoleCookieMiddleware(): MiddlewareHandler {
76
- return async (c, next) => {
77
- const override = new URL(c.req.url).searchParams.get('role');
78
- if (override) {
79
- if (override.toLowerCase() === ROLE_RESET_VALUE) {
80
- setCookie(c, MOCK_ROLE_COOKIE, '', { path: '/', maxAge: 0 });
81
- } else if (parseRole(override)) {
82
- setCookie(c, MOCK_ROLE_COOKIE, override.toLowerCase(), {
83
- path: '/',
84
- sameSite: 'Lax',
85
- });
176
+ /**
177
+ * Turns ?role= and, with the roster on, ?user= and ?ttl= into cookies;
178
+ * "default" clears. ?ttl= seconds gives the session an expiry, which is
179
+ * what drives stream rotation and the 401 that follows it.
180
+ */
181
+ cookieMiddleware(): MiddlewareHandler {
182
+ return async (c, next) => {
183
+ const params = new URL(c.req.url).searchParams;
184
+ applyOverride(c, params.get('role'), MOCK_ROLE_COOKIE, (value) =>
185
+ parseRole(value) ? value.toLowerCase() : null,
186
+ );
187
+ if (this.userConfig.roster) {
188
+ applyOverride(c, params.get('user'), MOCK_USER_COOKIE, (value) =>
189
+ parseUser(value) ? value.toLowerCase() : null,
190
+ );
191
+ applyOverride(c, params.get('ttl'), MOCK_EXP_COOKIE, toDeadline);
86
192
  }
87
- }
88
- await next();
89
- };
193
+ await next();
194
+ };
195
+ }
90
196
  }
package/src/auth/types.ts CHANGED
@@ -13,6 +13,13 @@
13
13
 
14
14
  import type { UserInfo } from '../types.js';
15
15
 
16
+ export interface AuthResult extends UserInfo {
17
+ /** Credential deadline (epoch seconds). Always present: a provider whose
18
+ * sessions do not expire says so with a deadline far enough out to be
19
+ * inert, rather than leaving every consumer to handle its absence. */
20
+ exp: number;
21
+ }
22
+
16
23
  export interface AuthProvider {
17
- authenticate(req: Request): Promise<UserInfo | null>;
24
+ authenticate(req: Request): Promise<AuthResult | null>;
18
25
  }
@@ -13,13 +13,11 @@
13
13
 
14
14
  import { type Context, Hono } from 'hono';
15
15
  import type { ContentfulStatusCode } from 'hono/utils/http-status';
16
- import { streamSSE } from 'hono/streaming';
17
16
  import { getCardDetails } from './lib.js';
18
17
  import * as cardService from './service.js';
19
18
  import { isSSGContext, ssgParams } from 'hono/ssg';
20
19
  import type { AppContext } from '../../types.js';
21
20
  import { UserRole } from '../../types.js';
22
- import { presenceStore } from './presence.js';
23
21
  import { requireRole } from '../../middleware/auth.js';
24
22
  import { zValidator } from '../../middleware/zvalidator.js';
25
23
  import {
@@ -32,6 +30,10 @@ import {
32
30
 
33
31
  const router = new Hono();
34
32
 
33
+ function notifyCardsUpdated(c: Context, keys: string[]) {
34
+ c.get('events').cardsUpdated(keys, c.get('user'));
35
+ }
36
+
35
37
  /**
36
38
  * @swagger
37
39
  * /api/cards:
@@ -240,7 +242,9 @@ router.patch('/:key', requireRole(UserRole.Editor), async (c) => {
240
242
  const body = await c.req.json();
241
243
 
242
244
  try {
243
- await cardService.updateCard(commands, key, body);
245
+ if (await cardService.updateCard(commands, key, body)) {
246
+ notifyCardsUpdated(c, [key]);
247
+ }
244
248
  const result = await getCardDetails(
245
249
  c.get('commands'),
246
250
  key,
@@ -400,6 +404,7 @@ router.post('/:key/attachments', requireRole(UserRole.Editor), async (c) => {
400
404
  key,
401
405
  files as File[],
402
406
  );
407
+ notifyCardsUpdated(c, [key]);
403
408
  return c.json(result);
404
409
  } catch (error) {
405
410
  return c.json(
@@ -451,6 +456,7 @@ router.delete(
451
456
  key,
452
457
  filename,
453
458
  );
459
+ notifyCardsUpdated(c, [key]);
454
460
  return c.json(result);
455
461
  } catch (error) {
456
462
  return c.json(
@@ -466,54 +472,6 @@ router.delete(
466
472
  },
467
473
  );
468
474
 
469
- /**
470
- * @swagger
471
- * /api/cards/{key}/attachments/{filename}/open:
472
- * post:
473
- * summary: Open an attachment using the system's default application
474
- * parameters:
475
- * - name: key
476
- * in: path
477
- * required: true
478
- * schema:
479
- * type: string
480
- * - name: filename
481
- * in: path
482
- * required: true
483
- * schema:
484
- * type: string
485
- * responses:
486
- * 200:
487
- * description: Attachment opened successfully
488
- * 400:
489
- * description: Invalid request
490
- * 500:
491
- * description: Server error
492
- */
493
- router.post(
494
- '/:key/attachments/:filename/open',
495
- requireRole(UserRole.Reader),
496
- async (c) => {
497
- const commands = c.get('commands');
498
- const { key, filename } = c.req.param();
499
-
500
- try {
501
- const result = await cardService.openAttachment(commands, key, filename);
502
- return c.json(result);
503
- } catch (error) {
504
- return c.json(
505
- {
506
- error:
507
- error instanceof Error
508
- ? error.message
509
- : 'Failed to open attachment',
510
- },
511
- 500,
512
- );
513
- }
514
- },
515
- );
516
-
517
475
  /**
518
476
  * @swagger
519
477
  * /api/cards/{key}/parse:
@@ -613,6 +571,7 @@ router.post(
613
571
  direction,
614
572
  description,
615
573
  );
574
+ notifyCardsUpdated(c, [key, toCard]);
616
575
  return c.json(result);
617
576
  } catch (error) {
618
577
  return c.json(
@@ -675,6 +634,7 @@ router.delete(
675
634
  direction,
676
635
  description,
677
636
  );
637
+ notifyCardsUpdated(c, [key, toCard]);
678
638
  return c.json(result);
679
639
  } catch (error) {
680
640
  return c.json(
@@ -761,6 +721,7 @@ router.put(
761
721
  description,
762
722
  previousDescription,
763
723
  );
724
+ notifyCardsUpdated(c, [key, toCard, previousToCard]);
764
725
  return c.json(result);
765
726
  } catch (error) {
766
727
  return c.json(
@@ -843,55 +804,4 @@ router.get(
843
804
  }
844
805
  },
845
806
  );
846
- /**
847
- * @swagger
848
- * /api/cards/{key}/presence:
849
- * get:
850
- * summary: SSE stream of users currently viewing or editing this card
851
- * parameters:
852
- * - name: key
853
- * in: path
854
- * required: true
855
- * description: Card key (string)
856
- * - name: mode
857
- * in: query
858
- * required: false
859
- * schema:
860
- * type: string
861
- * enum: [viewing, editing]
862
- * description: Whether the user is viewing or editing (default: viewing)
863
- * responses:
864
- * 200:
865
- * description: SSE stream with presence events
866
- */
867
- router.get('/:key/presence', requireRole(UserRole.Reader), (c) => {
868
- const key = c.req.param('key');
869
- const mode = c.req.query('mode') === 'editing' ? 'editing' : 'viewing';
870
- const user = c.get('user');
871
-
872
- if (!key) {
873
- return c.text('No card key', 400);
874
- }
875
-
876
- return streamSSE(c, async (stream) => {
877
- const connId = presenceStore.add(
878
- key,
879
- user,
880
- mode,
881
- (data) => void stream.writeSSE(data),
882
- );
883
-
884
- let aborted = false;
885
- stream.onAbort(() => {
886
- aborted = true;
887
- presenceStore.remove(key, connId);
888
- });
889
-
890
- // Keep connection alive with periodic heartbeat
891
- while (!aborted) {
892
- await stream.write(': hb\n\n');
893
- await stream.sleep(30000);
894
- }
895
- });
896
- });
897
807
  export default router;
@@ -11,7 +11,6 @@
11
11
  License along with this program. If not, see <https://www.gnu.org/licenses/>.
12
12
  */
13
13
 
14
- import { convert } from '@asciidoctor/core';
15
14
  import type { Card } from '@cyberismo/data-handler/interfaces/project-interfaces';
16
15
  import { type CommandManager, evaluateMacros } from '@cyberismo/data-handler';
17
16
  import { preprocessMermaidBlocksForHtml } from '@cyberismo/data-handler/utils/mermaid-renderer';
@@ -70,17 +69,6 @@ export async function getCardDetails(
70
69
  staticMode ? 'staticSite' : 'inject',
71
70
  );
72
71
 
73
- const projectPrefix = commands.project.projectPrefix;
74
- const htmlContent = (
75
- await convert(asciidocContent, {
76
- safe: 'safe',
77
- attributes: {
78
- imagesdir: `/api/projects/${projectPrefix}/cards/${key}/a`,
79
- icons: 'font',
80
- },
81
- })
82
- ).toString();
83
-
84
72
  if (raw) {
85
73
  if (!cardDetailsResponse.metadata) {
86
74
  throw new Error('Card has no metadata');
@@ -143,7 +131,7 @@ export async function getCardDetails(
143
131
  editContent: [],
144
132
  },
145
133
  rawContent: cardDetailsResponse.content || '',
146
- parsedContent: htmlContent,
134
+ adocContent: asciidocContent,
147
135
  attachments: cardDetailsResponse.attachments,
148
136
  path: cardDetailsResponse.path,
149
137
  },
@@ -165,7 +153,7 @@ export async function getCardDetails(
165
153
  data: {
166
154
  ...card[0],
167
155
  rawContent: cardDetailsResponse.content || '',
168
- parsedContent: htmlContent,
156
+ adocContent: asciidocContent,
169
157
  attachments: cardDetailsResponse.attachments,
170
158
  path: cardDetailsResponse.path,
171
159
  },
@@ -39,12 +39,20 @@ export const updateLinkSchema = z.object({
39
39
  previousDirection: linkDirection,
40
40
  previousDescription: z.string().optional(),
41
41
  });
42
+
43
+ // Interpolated into the exported document's AsciiDoc header, where a line break
44
+ // starts a new attribute entry.
45
+ const headerValue = z
46
+ .string()
47
+ .min(1)
48
+ .regex(/^[^\r\n]*$/, 'must not contain line breaks');
49
+
42
50
  export const exportCardPdfSchema = z.object({
43
- title: z.string().min(1),
44
- name: z.string().min(1),
51
+ title: headerValue,
52
+ name: headerValue,
45
53
  cardKey: z.string(),
46
54
  exportChildCards: z.boolean(),
47
- version: z.string().min(1).optional(),
55
+ version: headerValue.optional(),
48
56
  });
49
57
 
50
58
  export type ExportCardPdfRequestBody = z.infer<typeof exportCardPdfSchema>;
@@ -11,7 +11,6 @@
11
11
  License along with this program. If not, see <https://www.gnu.org/licenses/>.
12
12
  */
13
13
 
14
- import { convert } from '@asciidoctor/core';
15
14
  import type {
16
15
  ExportPdfOptions,
17
16
  MetadataContent,
@@ -51,13 +50,16 @@ export async function updateCard(
51
50
  key: string,
52
51
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
53
52
  body: any,
54
- ) {
53
+ ): Promise<boolean> {
54
+ let written = false;
55
55
  await commands.atomic(async () => {
56
56
  if (body.state) {
57
57
  await commands.transitionCmd.cardTransition(key, body.state);
58
+ written = true;
58
59
  }
59
60
  if (body.content != null) {
60
61
  await commands.editCmd.editCardContent(key, body.content);
62
+ written = true;
61
63
  }
62
64
  if (body.metadata) {
63
65
  for (const [metadataKey, metadataValue] of Object.entries(
@@ -68,15 +70,19 @@ export async function updateCard(
68
70
  metadataKey,
69
71
  metadataValue as MetadataContent,
70
72
  );
73
+ written = true;
71
74
  }
72
75
  }
73
76
  if (body.parent) {
74
77
  await commands.moveCmd.moveCard(key, body.parent);
78
+ written = true;
75
79
  }
76
80
  if (body.index != null) {
77
81
  await commands.moveCmd.rankByIndex(key, body.index);
82
+ written = true;
78
83
  }
79
84
  }, `Update card ${key}`);
85
+ return written;
80
86
  }
81
87
 
82
88
  export async function deleteCard(commands: CommandManager, key: string) {
@@ -142,16 +148,6 @@ export async function removeAttachment(
142
148
  return { message: 'Attachment removed successfully' };
143
149
  }
144
150
 
145
- export async function openAttachment(
146
- commands: CommandManager,
147
- key: string,
148
- filename: string,
149
- ) {
150
- validateAttachmentFileName(filename);
151
- await commands.showCmd.openAttachment(key, filename);
152
- return { message: 'Attachment opened successfully' };
153
- }
154
-
155
151
  export async function parseContent(
156
152
  commands: CommandManager,
157
153
  key: string,
@@ -181,18 +177,7 @@ export async function parseContent(
181
177
  'inject',
182
178
  );
183
179
 
184
- const projectPrefix = commands.project.projectPrefix;
185
- const parsedContent = (
186
- await convert(asciidocContent, {
187
- safe: 'safe',
188
- attributes: {
189
- imagesdir: `/api/projects/${projectPrefix}/cards/${key}/a`,
190
- icons: 'font',
191
- },
192
- })
193
- ).toString();
194
-
195
- return { parsedContent };
180
+ return { adocContent: asciidocContent };
196
181
  });
197
182
  }
198
183
 
@@ -0,0 +1,112 @@
1
+ /**
2
+ Cyberismo
3
+ Copyright © Cyberismo Ltd and contributors 2026
4
+ This program is free software: you can redistribute it and/or modify it under
5
+ the terms of the GNU Affero General Public License version 3 as published by
6
+ the Free Software Foundation.
7
+ This program is distributed in the hope that it will be useful, but WITHOUT
8
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
9
+ FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
10
+ details. You should have received a copy of the GNU Affero General Public
11
+ License along with this program. If not, see <https://www.gnu.org/licenses/>.
12
+ */
13
+
14
+ import { Hono } from 'hono';
15
+ import { disableSSG } from 'hono/ssg';
16
+ import { streamSSE, type SSEMessage } from 'hono/streaming';
17
+ import { isAtLeastRole, requireRole } from '../../middleware/auth.js';
18
+ import { zValidator } from '../../middleware/zvalidator.js';
19
+ import { UserRole } from '../../types.js';
20
+ import { computeLifetimeMs } from './lifetime.js';
21
+ import { presenceSchema } from './schema.js';
22
+
23
+ const HEARTBEAT_INTERVAL_MS = 30_000;
24
+
25
+ const router = new Hono();
26
+
27
+ router.get('/', disableSSG(), requireRole(UserRole.Reader), (c) => {
28
+ const events = c.get('events');
29
+ const user = c.get('user');
30
+ const capabilities = {
31
+ canSeeIdentity: isAtLeastRole(user.role, UserRole.Editor),
32
+ canDeclareEditing: isAtLeastRole(user.role, UserRole.Editor),
33
+ canSeePresence: isAtLeastRole(user.role, UserRole.Editor),
34
+ };
35
+ return streamSSE(c, async (stream) => {
36
+ if (stream.aborted) return;
37
+ let finish: () => void = () => {};
38
+ const closed = new Promise<void>((resolve) => {
39
+ finish = resolve;
40
+ });
41
+ let stopped = false;
42
+ let release = () => {};
43
+ const stop = () => {
44
+ if (stopped) return;
45
+ stopped = true;
46
+ release();
47
+ finish();
48
+ };
49
+ // Ends the stream after one last message. Deliberately does not call
50
+ // `release()`: a rotating connection has been retired, and disconnecting
51
+ // it would drop the presence entry that bridges the client's reconnect.
52
+ const terminate = (message: SSEMessage) => {
53
+ if (stopped) return;
54
+ stopped = true;
55
+ clearInterval(heartbeat);
56
+ clearTimeout(rotate);
57
+ void stream.writeSSE(message).then(finish);
58
+ };
59
+ // Hono swallows stream write errors; `onAbort` is the only teardown signal.
60
+ stream.onAbort(stop);
61
+ const connectionId = events.connect(user, capabilities, {
62
+ send: (message) => void stream.writeSSE(message),
63
+ close: stop,
64
+ terminate,
65
+ });
66
+ release = () => events.disconnect(connectionId);
67
+ const heartbeat = setInterval(
68
+ () => void stream.writeSSE({ event: 'hb', data: '' }),
69
+ HEARTBEAT_INTERVAL_MS,
70
+ );
71
+ const rotate = setTimeout(
72
+ () => {
73
+ events.retire(connectionId);
74
+ terminate({ event: 'rotating', data: '' });
75
+ },
76
+ computeLifetimeMs(c.get('tokenExp')),
77
+ );
78
+ try {
79
+ await closed;
80
+ } finally {
81
+ clearInterval(heartbeat);
82
+ clearTimeout(rotate);
83
+ stop();
84
+ }
85
+ });
86
+ });
87
+
88
+ router.put(
89
+ '/presence',
90
+ requireRole(UserRole.Reader),
91
+ zValidator('json', presenceSchema),
92
+ (c) => {
93
+ const { connectionId, sequence, cardKey, mode } = c.req.valid('json');
94
+ if (cardKey && !c.get('commands').project.hasCard(cardKey)) {
95
+ return c.json({ error: 'Card not found' }, 404);
96
+ }
97
+ const events = c.get('events');
98
+ const known = events.update(
99
+ connectionId,
100
+ c.get('user').id,
101
+ sequence,
102
+ cardKey,
103
+ mode,
104
+ );
105
+ if (!known) {
106
+ return c.json({ error: 'Presence connection not found' }, 404);
107
+ }
108
+ return c.body(null, 204);
109
+ },
110
+ );
111
+
112
+ export default router;