chainlesschain 0.158.0 → 0.160.1

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 (331) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/.build-hash +1 -1
  3. package/src/assets/web-panel/assets/AIOps-BHiKMxFI.js +1 -0
  4. package/src/assets/web-panel/assets/AIOps-Cchx1iXI.css +1 -0
  5. package/src/assets/web-panel/assets/ActionButton-5QD5uzWP.js +1 -0
  6. package/src/assets/web-panel/assets/Analytics-C1-TXmTC.css +1 -0
  7. package/src/assets/web-panel/assets/Analytics-D1JxsGBN.js +3 -0
  8. package/src/assets/web-panel/assets/AppLayout-CtGprHSx.css +1 -0
  9. package/src/assets/web-panel/assets/AppLayout-DykU9tOE.js +1 -0
  10. package/src/assets/web-panel/assets/Audit-6ZMsXmrO.css +1 -0
  11. package/src/assets/web-panel/assets/Audit-TGBqld9c.js +1 -0
  12. package/src/assets/web-panel/assets/Backup-CY9QozR7.css +1 -0
  13. package/src/assets/web-panel/assets/Backup-DjpzIwA6.js +1 -0
  14. package/src/assets/web-panel/assets/BaseInput-joQlVauq.js +1 -0
  15. package/src/assets/web-panel/assets/Chat-9TYfosy-.js +2 -0
  16. package/src/assets/web-panel/assets/{Chat-DmX5bWvL.css → Chat-ByiYUboW.css} +1 -1
  17. package/src/assets/web-panel/assets/Checkbox-OEOFA9GM.js +1 -0
  18. package/src/assets/web-panel/assets/Codegen-5H5UgHJu.js +1 -0
  19. package/src/assets/web-panel/assets/Codegen-BLP7id2a.css +1 -0
  20. package/src/assets/web-panel/assets/Col-BnLUipDp.js +1 -0
  21. package/src/assets/web-panel/assets/Community-BF3R5GAl.js +1 -0
  22. package/src/assets/web-panel/assets/Community-C2RejeOY.css +1 -0
  23. package/src/assets/web-panel/assets/Compact-C1EkTFek.js +1 -0
  24. package/src/assets/web-panel/assets/Compliance-CpP-ODRU.js +1 -0
  25. package/src/assets/web-panel/assets/Compliance-DOys4Ov1.css +1 -0
  26. package/src/assets/web-panel/assets/{Cowork-pgKzBNDo.js → Cowork-CCgGSKVR.js} +3 -3
  27. package/src/assets/web-panel/assets/Cron-CZ5pjRxn.js +2 -0
  28. package/src/assets/web-panel/assets/Crosschain-C0P-5sm3.js +1 -0
  29. package/src/assets/web-panel/assets/Crosschain-C7Le4Pte.css +1 -0
  30. package/src/assets/web-panel/assets/DID-BX6k3jZi.css +1 -0
  31. package/src/assets/web-panel/assets/DID-DPZKMApP.js +2 -0
  32. package/src/assets/web-panel/assets/Dashboard-G-BDDAov.js +3 -0
  33. package/src/assets/web-panel/assets/Dashboard-MFDcsVcM.css +1 -0
  34. package/src/assets/web-panel/assets/Dropdown-CeywCcVQ.js +1 -0
  35. package/src/assets/web-panel/assets/Federation-CgmfLbx1.css +1 -0
  36. package/src/assets/web-panel/assets/Federation-DuFRY867.js +1 -0
  37. package/src/assets/web-panel/assets/{FormItemContext-CnzbixSE.js → FormItemContext-CFSiPqbu.js} +1 -1
  38. package/src/assets/web-panel/assets/Git-CjVRJDLg.js +2 -0
  39. package/src/assets/web-panel/assets/{Git-DGcuBXST.css → Git-DPuaGtg7.css} +1 -1
  40. package/src/assets/web-panel/assets/Governance-BoipmXaM.css +1 -0
  41. package/src/assets/web-panel/assets/Governance-C0lyocJc.js +1 -0
  42. package/src/assets/web-panel/assets/Inference-BVSAexgk.js +1 -0
  43. package/src/assets/web-panel/assets/Inference-BWxYJF9-.css +1 -0
  44. package/src/assets/web-panel/assets/KnowledgeGraph-CztPDA96.css +1 -0
  45. package/src/assets/web-panel/assets/KnowledgeGraph-SE4jCwIn.js +1 -0
  46. package/src/assets/web-panel/assets/Logs-BD5C-wTx.js +2 -0
  47. package/src/assets/web-panel/assets/Marketplace-CL93dFBs.js +1 -0
  48. package/src/assets/web-panel/assets/Marketplace-Djp5q9dS.css +1 -0
  49. package/src/assets/web-panel/assets/McpTools-CDjHmzxH.css +1 -0
  50. package/src/assets/web-panel/assets/McpTools-x-Tibae-.js +5 -0
  51. package/src/assets/web-panel/assets/Memory-Bcc2hxOA.css +1 -0
  52. package/src/assets/web-panel/assets/Memory-CR8LXq37.js +2 -0
  53. package/src/assets/web-panel/assets/Mtc-C-PfF5B3.css +1 -0
  54. package/src/assets/web-panel/assets/Mtc-CEtRtMcc.js +1 -0
  55. package/src/assets/web-panel/assets/NLProgramming-B09F6gt2.js +1 -0
  56. package/src/assets/web-panel/assets/NLProgramming-CLOvy-35.css +1 -0
  57. package/src/assets/web-panel/assets/Notes-BYIn2GOe.js +7 -0
  58. package/src/assets/web-panel/assets/Notes-DKkPfXlY.css +1 -0
  59. package/src/assets/web-panel/assets/Organization-BX-cIO8M.css +1 -0
  60. package/src/assets/web-panel/assets/Organization-CybJTFN9.js +4 -0
  61. package/src/assets/web-panel/assets/Overflow-W4YLQ7yY.js +1 -0
  62. package/src/assets/web-panel/assets/{OverrideContext-DEW2m7K6.js → OverrideContext-Nubhv68k.js} +1 -1
  63. package/src/assets/web-panel/assets/P2P-Cx88UaiD.css +1 -0
  64. package/src/assets/web-panel/assets/P2P-kVj43R4j.js +2 -0
  65. package/src/assets/web-panel/assets/Permissions-CfYE4XFJ.js +4 -0
  66. package/src/assets/web-panel/assets/Pipeline-BVLo32Ak.js +1 -0
  67. package/src/assets/web-panel/assets/Pipeline-DxkXqrH2.css +1 -0
  68. package/src/assets/web-panel/assets/Privacy-CrsfSFKd.css +1 -0
  69. package/src/assets/web-panel/assets/Privacy-Efyb3xpJ.js +1 -0
  70. package/src/assets/web-panel/assets/ProjectSettings-cBqrIhNN.js +2 -0
  71. package/src/assets/web-panel/assets/Projects-B5IgXt-x.css +1 -0
  72. package/src/assets/web-panel/assets/Projects-BYY38oZd.js +2 -0
  73. package/src/assets/web-panel/assets/Providers-BsS27cWs.js +2 -0
  74. package/src/assets/web-panel/assets/QuickAsk-6FgX9DC6.js +1 -0
  75. package/src/assets/web-panel/assets/Recommend-BvMXwWFN.js +1 -0
  76. package/src/assets/web-panel/assets/Recommend-CH6wKzGo.css +1 -0
  77. package/src/assets/web-panel/assets/Reputation-D6VPNEd0.css +1 -0
  78. package/src/assets/web-panel/assets/Reputation-DmwTtBfl.js +1 -0
  79. package/src/assets/web-panel/assets/Row-N-X7EJ3w.js +1 -0
  80. package/src/assets/web-panel/assets/RssFeed-D9TjnwgF.js +3 -0
  81. package/src/assets/web-panel/assets/Search-B6RalzTB.css +1 -0
  82. package/src/assets/web-panel/assets/Search-Hapv-QkV.js +1 -0
  83. package/src/assets/web-panel/assets/Security-13K57V_v.css +1 -0
  84. package/src/assets/web-panel/assets/Security-DWbFJK10.js +4 -0
  85. package/src/assets/web-panel/assets/Services-BPUmhVoH.js +2 -0
  86. package/src/assets/web-panel/assets/Skeleton-Bo5qPHbE.js +8 -0
  87. package/src/assets/web-panel/assets/Skills-JJ8uInMW.js +1 -0
  88. package/src/assets/web-panel/assets/Sla-CEDF9zdV.js +1 -0
  89. package/src/assets/web-panel/assets/Sla-K19oOyQc.css +1 -0
  90. package/src/assets/web-panel/assets/SpeechSettings-DYPJTDKz.css +1 -0
  91. package/src/assets/web-panel/assets/SpeechSettings-oIoX_vCx.js +1 -0
  92. package/src/assets/web-panel/assets/Tasks-Cx5wgv5Z.js +1 -0
  93. package/src/assets/web-panel/assets/Templates-BWTV8-2E.css +1 -0
  94. package/src/assets/web-panel/assets/Templates-BomcBlkN.js +1 -0
  95. package/src/assets/web-panel/assets/Tenant-BxSQZUNh.js +1 -0
  96. package/src/assets/web-panel/assets/Tenant-D3zkSAV0.css +1 -0
  97. package/src/assets/web-panel/assets/Tokens-BBOdNRHQ.css +1 -0
  98. package/src/assets/web-panel/assets/Tokens-BlPPoB3C.js +1 -0
  99. package/src/assets/web-panel/assets/Trigger-Bhjmjsc5.js +1 -0
  100. package/src/assets/web-panel/assets/Trust-DeOo0xAh.css +1 -0
  101. package/src/assets/web-panel/assets/Trust-Dsjv7rkb.js +1 -0
  102. package/src/assets/web-panel/assets/UkeySign-Cux8_Ib_.js +1 -0
  103. package/src/assets/web-panel/assets/VideoEditing-BsVR1PN8.js +1 -0
  104. package/src/assets/web-panel/assets/VideoEditing-DksiizfS.css +1 -0
  105. package/src/assets/web-panel/assets/Wallet-dcRAYsdL.js +4 -0
  106. package/src/assets/web-panel/assets/Wallet-gR0ZvZbK.css +1 -0
  107. package/src/assets/web-panel/assets/WebAuthn-SSh5VhVO.css +1 -0
  108. package/src/assets/web-panel/assets/WebAuthn-oqIS5PCi.js +5 -0
  109. package/src/assets/web-panel/assets/WorkflowEditor-C_fYMBvB.js +1 -0
  110. package/src/assets/web-panel/assets/WorkflowEditor-IiwsD8Kh.css +1 -0
  111. package/src/assets/web-panel/assets/{chat-CVn-r3oV.js → chat-BQ-Nk2XY.js} +1 -1
  112. package/src/assets/web-panel/assets/{collapseMotion-D3P4mRvK.js → collapseMotion-BIjDVXtT.js} +1 -1
  113. package/src/assets/web-panel/assets/{colors-BiB5aDao.js → colors-D2P6CqS5.js} +1 -1
  114. package/src/assets/web-panel/assets/{compact-item-CHderV9n.js → compact-item-CG7qutT_.js} +1 -1
  115. package/src/assets/web-panel/assets/{createContext-DMZ-5dy2.js → createContext-y4UPKgbA.js} +1 -1
  116. package/src/assets/web-panel/assets/echarts-DmBLM6YO.js +19 -0
  117. package/src/assets/web-panel/assets/{hasIn-BKDBYP3s.js → hasIn-Butbu9jZ.js} +1 -1
  118. package/src/assets/web-panel/assets/icons-DvZE-RKs.js +57 -0
  119. package/src/assets/web-panel/assets/index-38mVlGHc.js +1 -0
  120. package/src/assets/web-panel/assets/{index-C1p_dZw0.js → index-89HJLKZ-.js} +3 -3
  121. package/src/assets/web-panel/assets/index-B4Jfv4EB.js +1 -0
  122. package/src/assets/web-panel/assets/index-B5FRjJMb.js +1 -0
  123. package/src/assets/web-panel/assets/{index-DvI8Xtzv.js → index-B6U6cYUa.js} +8 -8
  124. package/src/assets/web-panel/assets/index-B7FV5EnN.js +1 -0
  125. package/src/assets/web-panel/assets/index-BCQ0WlB2.js +1 -0
  126. package/src/assets/web-panel/assets/index-BEfvpbz-.js +1 -0
  127. package/src/assets/web-panel/assets/index-BJN_3RTO.js +1 -0
  128. package/src/assets/web-panel/assets/index-BQfow_sh.js +1 -0
  129. package/src/assets/web-panel/assets/index-BQr8Y0o5.js +1 -0
  130. package/src/assets/web-panel/assets/index-BYZPJS7A.js +1 -0
  131. package/src/assets/web-panel/assets/index-Bs9aHxDD.js +13 -0
  132. package/src/assets/web-panel/assets/index-BtuwtDUE.js +1 -0
  133. package/src/assets/web-panel/assets/index-BvJgRWBq.js +3 -0
  134. package/src/assets/web-panel/assets/index-C1mK1Ga3.js +1 -0
  135. package/src/assets/web-panel/assets/index-C1ucrJLg.js +1 -0
  136. package/src/assets/web-panel/assets/index-C2K61jP8.js +55 -0
  137. package/src/assets/web-panel/assets/index-CAeKBs9n.js +1 -0
  138. package/src/assets/web-panel/assets/{index-BBWUR4u4.js → index-CWh3IxEh.js} +2 -2
  139. package/src/assets/web-panel/assets/{index-CUT3Nmzx.js → index-CYlDKn3O.js} +3 -3
  140. package/src/assets/web-panel/assets/index-C_8hWf5_.js +6 -0
  141. package/src/assets/web-panel/assets/index-Cc77JZKd.js +1 -0
  142. package/src/assets/web-panel/assets/index-Ceaxjpqh.js +13 -0
  143. package/src/assets/web-panel/assets/index-CfX1DEtk.css +1 -0
  144. package/src/assets/web-panel/assets/index-Ci6jXp3l.js +7 -0
  145. package/src/assets/web-panel/assets/index-CyqU4Tck.js +65 -0
  146. package/src/assets/web-panel/assets/{index-7fxCGMoL.js → index-DLMJy9pE.js} +6 -6
  147. package/src/assets/web-panel/assets/index-DdgjeX4z.js +1 -0
  148. package/src/assets/web-panel/assets/index-DnI4Aq0q.js +3 -0
  149. package/src/assets/web-panel/assets/index-DrVnyYpX.js +1 -0
  150. package/src/assets/web-panel/assets/index-DtNHlrxp.js +1 -0
  151. package/src/assets/web-panel/assets/index-Dx_ZTZo_.js +1 -0
  152. package/src/assets/web-panel/assets/{index-CBYNsy51.js → index-YmGOWX7h.js} +2 -2
  153. package/src/assets/web-panel/assets/index-gWmZm8_Q.js +21 -0
  154. package/src/assets/web-panel/assets/index-hSilB_Q-.js +12 -0
  155. package/src/assets/web-panel/assets/index-qXvwlbkq.js +3 -0
  156. package/src/assets/web-panel/assets/index-rIbVsjde.js +12 -0
  157. package/src/assets/web-panel/assets/{index-B7fivpG2.js → index-vC5cTycG.js} +2 -2
  158. package/src/assets/web-panel/assets/{initDefaultProps-B2vQoQga.js → initDefaultProps-CZRZ-1bk.js} +1 -1
  159. package/src/assets/web-panel/assets/motion-CvU8SiWF.js +11 -0
  160. package/src/assets/web-panel/assets/move-ipAfWhya.js +4 -0
  161. package/src/assets/web-panel/assets/mtc-parser-pGMSt10g.js +1 -0
  162. package/src/assets/web-panel/assets/{omit-DvLh7wH1.js → omit-D6bJEjz9.js} +1 -1
  163. package/src/assets/web-panel/assets/{pickAttrs-BsseBclw.js → pickAttrs-Dpvzf7sL.js} +1 -1
  164. package/src/assets/web-panel/assets/{placementArrow-XLLtr9qD.js → placementArrow-D_tEolP1.js} +1 -1
  165. package/src/assets/web-panel/assets/{responsiveObserve-ByCb5zZK.js → responsiveObserve-BEFI7neO.js} +1 -1
  166. package/src/assets/web-panel/assets/slide-Bte_KOqM.js +4 -0
  167. package/src/assets/web-panel/assets/statusUtils-K4xaDRuO.js +1 -0
  168. package/src/assets/web-panel/assets/{styleChecker-Cz5mJgc8.js → styleChecker-Cl9YgOVY.js} +1 -1
  169. package/src/assets/web-panel/assets/useFlexGapSupport-DNstl1wK.js +1 -0
  170. package/src/assets/web-panel/assets/useFs-BD-YRwbU.js +1 -0
  171. package/src/assets/web-panel/assets/{useMergedState-DbHXwBgv.js → useMergedState-TP9VIF2K.js} +1 -1
  172. package/src/assets/web-panel/assets/{useRefs-DRyTVik7.js → useRefs-BhIz_lC3.js} +1 -1
  173. package/src/assets/web-panel/assets/useShellMode-CgR0wCYM.js +1 -0
  174. package/src/assets/web-panel/assets/{useState-DoAhV80c.js → useState-CpKsyozn.js} +1 -1
  175. package/src/assets/web-panel/assets/vendor-B6ToihkA.js +1 -0
  176. package/src/assets/web-panel/assets/vnode-ChB-8cXr.js +1 -0
  177. package/src/assets/web-panel/assets/ws-D_5-FRIb.js +1 -0
  178. package/src/assets/web-panel/assets/zoom-meTNBulL.js +4 -0
  179. package/src/assets/web-panel/index.html +4 -4
  180. package/src/commands/audit.js +296 -0
  181. package/src/commands/init.js +16 -1
  182. package/src/commands/mtc.js +1401 -86
  183. package/src/gateways/ws/ws-session-gateway.js +12 -1
  184. package/src/lib/audit-mtc.js +504 -0
  185. package/src/lib/listen-with-port-fallback.js +103 -0
  186. package/src/runtime/agent-runtime.js +80 -18
  187. package/src/assets/web-panel/assets/AIOps-9QEKGr43.js +0 -1
  188. package/src/assets/web-panel/assets/AIOps-Bfzpnnlg.css +0 -1
  189. package/src/assets/web-panel/assets/ActionButton-BQElVBgD.js +0 -1
  190. package/src/assets/web-panel/assets/Analytics-BFI7jbwM.css +0 -1
  191. package/src/assets/web-panel/assets/Analytics-DbZbFR_a.js +0 -3
  192. package/src/assets/web-panel/assets/AppLayout-BbfBwVfE.js +0 -1
  193. package/src/assets/web-panel/assets/AppLayout-CKMl4NLf.css +0 -1
  194. package/src/assets/web-panel/assets/Audit-DzddAZVx.js +0 -1
  195. package/src/assets/web-panel/assets/Audit-L4_ApS01.css +0 -1
  196. package/src/assets/web-panel/assets/Backup-D46qSgzQ.js +0 -1
  197. package/src/assets/web-panel/assets/Backup-fZqtfC1m.css +0 -1
  198. package/src/assets/web-panel/assets/BaseInput-60t9IMEz.js +0 -1
  199. package/src/assets/web-panel/assets/Chat-3Kj2JQP4.js +0 -2
  200. package/src/assets/web-panel/assets/Checkbox-Ba1Rf2Ea.js +0 -1
  201. package/src/assets/web-panel/assets/Codegen-AVAcL7NA.css +0 -1
  202. package/src/assets/web-panel/assets/Codegen-WiYmL3oB.js +0 -1
  203. package/src/assets/web-panel/assets/Col-D2zhpSqE.js +0 -1
  204. package/src/assets/web-panel/assets/Community-DgqmUGwT.js +0 -1
  205. package/src/assets/web-panel/assets/Community-DqDfLQui.css +0 -1
  206. package/src/assets/web-panel/assets/Compact-D_dbJDbi.js +0 -1
  207. package/src/assets/web-panel/assets/Compliance-BVcVb-e2.js +0 -1
  208. package/src/assets/web-panel/assets/Compliance-CKxw6vIq.css +0 -1
  209. package/src/assets/web-panel/assets/Cron-Cs5QaKTP.js +0 -2
  210. package/src/assets/web-panel/assets/Crosschain-DThGgQk8.css +0 -1
  211. package/src/assets/web-panel/assets/Crosschain-pjWfIM8s.js +0 -1
  212. package/src/assets/web-panel/assets/DID-BDvsVa08.css +0 -1
  213. package/src/assets/web-panel/assets/DID-DQtsjz3X.js +0 -2
  214. package/src/assets/web-panel/assets/Dashboard-Cviwdc26.css +0 -1
  215. package/src/assets/web-panel/assets/Dashboard-sqj42zKx.js +0 -3
  216. package/src/assets/web-panel/assets/Dropdown-BM_ljd87.js +0 -1
  217. package/src/assets/web-panel/assets/Federation-BftELHDw.css +0 -1
  218. package/src/assets/web-panel/assets/Federation-CId-3zn6.js +0 -1
  219. package/src/assets/web-panel/assets/Git-B38QeOPF.js +0 -2
  220. package/src/assets/web-panel/assets/Governance-BfmfQBGB.css +0 -1
  221. package/src/assets/web-panel/assets/Governance-DrGkAjd1.js +0 -1
  222. package/src/assets/web-panel/assets/Inference-BjkHB4sD.js +0 -1
  223. package/src/assets/web-panel/assets/Inference-EFFc7eNZ.css +0 -1
  224. package/src/assets/web-panel/assets/Keyframes-C7fCrnlS.js +0 -1
  225. package/src/assets/web-panel/assets/KnowledgeGraph-Trs1XJFK.js +0 -19
  226. package/src/assets/web-panel/assets/KnowledgeGraph-U8ps3aGJ.css +0 -1
  227. package/src/assets/web-panel/assets/Logs-B39LUZs9.js +0 -2
  228. package/src/assets/web-panel/assets/Marketplace-B-4uYu_j.css +0 -1
  229. package/src/assets/web-panel/assets/Marketplace-BnpOoITi.js +0 -1
  230. package/src/assets/web-panel/assets/McpTools-CyhSLDwf.css +0 -1
  231. package/src/assets/web-panel/assets/McpTools-DbwkdBUE.js +0 -4
  232. package/src/assets/web-panel/assets/Memory-BHXHpldt.js +0 -2
  233. package/src/assets/web-panel/assets/Memory-DRghrGJr.css +0 -1
  234. package/src/assets/web-panel/assets/NLProgramming-B3BALajs.js +0 -1
  235. package/src/assets/web-panel/assets/NLProgramming-jURs-f-a.css +0 -1
  236. package/src/assets/web-panel/assets/Notes-BG69sJKi.css +0 -1
  237. package/src/assets/web-panel/assets/Notes-u6lLTv0t.js +0 -2
  238. package/src/assets/web-panel/assets/Organization-CQ1X722j.js +0 -4
  239. package/src/assets/web-panel/assets/Organization-DdOOM4ic.css +0 -1
  240. package/src/assets/web-panel/assets/Overflow-DHKu8e8g.js +0 -1
  241. package/src/assets/web-panel/assets/P2P-BflX74Hm.js +0 -2
  242. package/src/assets/web-panel/assets/P2P-OEzOeMZX.css +0 -1
  243. package/src/assets/web-panel/assets/Permissions-B2XrAUBW.js +0 -4
  244. package/src/assets/web-panel/assets/Pipeline-C0K9-DnR.js +0 -1
  245. package/src/assets/web-panel/assets/Pipeline-DyqCLFVr.css +0 -1
  246. package/src/assets/web-panel/assets/Portal-CHsntPCQ.js +0 -1
  247. package/src/assets/web-panel/assets/Privacy-BKPoZngH.js +0 -1
  248. package/src/assets/web-panel/assets/Privacy-B_cAicd1.css +0 -1
  249. package/src/assets/web-panel/assets/ProjectSettings-C34C7hFt.js +0 -2
  250. package/src/assets/web-panel/assets/Projects-DLvUzeIB.js +0 -2
  251. package/src/assets/web-panel/assets/Projects-DxKelI5h.css +0 -1
  252. package/src/assets/web-panel/assets/Providers-BFiMVWaM.js +0 -2
  253. package/src/assets/web-panel/assets/Recommend-BmUhysGC.js +0 -1
  254. package/src/assets/web-panel/assets/Recommend-DgNSCgRX.css +0 -1
  255. package/src/assets/web-panel/assets/Reputation-BlfSvJww.js +0 -1
  256. package/src/assets/web-panel/assets/Reputation-y-46ThW8.css +0 -1
  257. package/src/assets/web-panel/assets/Row-Dl74576S.js +0 -1
  258. package/src/assets/web-panel/assets/RssFeed-9_MkDUxZ.js +0 -3
  259. package/src/assets/web-panel/assets/Search-BTk9rglb.css +0 -1
  260. package/src/assets/web-panel/assets/Search-CSRY_TYa.js +0 -1
  261. package/src/assets/web-panel/assets/Security-DfDoKfkl.js +0 -4
  262. package/src/assets/web-panel/assets/Security-Dwxw7rfP.css +0 -1
  263. package/src/assets/web-panel/assets/Services-7Blny0Jc.js +0 -2
  264. package/src/assets/web-panel/assets/Skeleton-6CF_3QUY.js +0 -8
  265. package/src/assets/web-panel/assets/Skills-DowQcRe1.js +0 -1
  266. package/src/assets/web-panel/assets/Sla-B93c8j92.js +0 -1
  267. package/src/assets/web-panel/assets/Sla-C1WYuQKf.css +0 -1
  268. package/src/assets/web-panel/assets/Tasks-Db9TRjE7.js +0 -1
  269. package/src/assets/web-panel/assets/Templates-DJ_hvs5c.js +0 -1
  270. package/src/assets/web-panel/assets/Templates-DOY_oZnm.css +0 -1
  271. package/src/assets/web-panel/assets/Tenant-BJr-h-_0.css +0 -1
  272. package/src/assets/web-panel/assets/Tenant-BXS8s6vl.js +0 -1
  273. package/src/assets/web-panel/assets/Tokens-CvmTVU7E.js +0 -1
  274. package/src/assets/web-panel/assets/Tokens-KvJRHQcl.css +0 -1
  275. package/src/assets/web-panel/assets/Trigger-BenSsaoN.js +0 -1
  276. package/src/assets/web-panel/assets/Trust-BLI308Ik.css +0 -1
  277. package/src/assets/web-panel/assets/Trust-HMz40LfZ.js +0 -1
  278. package/src/assets/web-panel/assets/VideoEditing-BA1N-5kq.css +0 -1
  279. package/src/assets/web-panel/assets/VideoEditing-DEhH42sI.js +0 -1
  280. package/src/assets/web-panel/assets/Wallet-AGwfOdTM.js +0 -4
  281. package/src/assets/web-panel/assets/Wallet-DnIumafl.css +0 -1
  282. package/src/assets/web-panel/assets/WebAuthn-CNPl2VQR.css +0 -1
  283. package/src/assets/web-panel/assets/WebAuthn-DK5tKWul.js +0 -5
  284. package/src/assets/web-panel/assets/WorkflowEditor-D5bX6woe.css +0 -1
  285. package/src/assets/web-panel/assets/WorkflowEditor-vlujdL6I.js +0 -1
  286. package/src/assets/web-panel/assets/_plugin-vue_export-helper-DlAUqK2U.js +0 -1
  287. package/src/assets/web-panel/assets/icons-ukMaZ3tx.js +0 -57
  288. package/src/assets/web-panel/assets/index-7pqZqPD2.js +0 -1
  289. package/src/assets/web-panel/assets/index-A4RpHMOa.js +0 -1
  290. package/src/assets/web-panel/assets/index-AIdelwOe.js +0 -12
  291. package/src/assets/web-panel/assets/index-B5oebI6W.js +0 -1
  292. package/src/assets/web-panel/assets/index-BBk2e7vJ.js +0 -13
  293. package/src/assets/web-panel/assets/index-BRRFK-im.js +0 -1
  294. package/src/assets/web-panel/assets/index-BVOI707f.js +0 -3
  295. package/src/assets/web-panel/assets/index-BadelvSE.js +0 -1
  296. package/src/assets/web-panel/assets/index-BfKFNptm.js +0 -14
  297. package/src/assets/web-panel/assets/index-BgDX-tzO.js +0 -1
  298. package/src/assets/web-panel/assets/index-BjSojphX.js +0 -21
  299. package/src/assets/web-panel/assets/index-BlXskar6.js +0 -1
  300. package/src/assets/web-panel/assets/index-BryAkWUJ.js +0 -1
  301. package/src/assets/web-panel/assets/index-CEpcKl17.js +0 -3
  302. package/src/assets/web-panel/assets/index-CbpCY2ch.js +0 -3
  303. package/src/assets/web-panel/assets/index-CyGyEIVX.css +0 -1
  304. package/src/assets/web-panel/assets/index-D3PGpBPm.js +0 -1
  305. package/src/assets/web-panel/assets/index-D3lD9wgX.js +0 -1
  306. package/src/assets/web-panel/assets/index-D5Beu9ZA.js +0 -1
  307. package/src/assets/web-panel/assets/index-DRRzH4Ge.js +0 -13
  308. package/src/assets/web-panel/assets/index-DRcTAUJA.js +0 -1
  309. package/src/assets/web-panel/assets/index-DZ8MjPLC.js +0 -7
  310. package/src/assets/web-panel/assets/index-DfcXDy2A.js +0 -36
  311. package/src/assets/web-panel/assets/index-DtDFSCfE.js +0 -12
  312. package/src/assets/web-panel/assets/index-DvfAkHw8.js +0 -1
  313. package/src/assets/web-panel/assets/index-DyXslU_B.js +0 -1
  314. package/src/assets/web-panel/assets/index-MoLxDrkb.js +0 -55
  315. package/src/assets/web-panel/assets/index-Ncxqj2UN.js +0 -1
  316. package/src/assets/web-panel/assets/index-U4Zd5IK6.js +0 -8
  317. package/src/assets/web-panel/assets/index-YgC7QI1N.js +0 -1
  318. package/src/assets/web-panel/assets/index-_sntSncx.js +0 -6
  319. package/src/assets/web-panel/assets/index-uujjC_4g.js +0 -1
  320. package/src/assets/web-panel/assets/index-yMt4C1Gh.js +0 -1
  321. package/src/assets/web-panel/assets/motion-D2eluOJu.js +0 -11
  322. package/src/assets/web-panel/assets/move-CgcvSoZ7.js +0 -4
  323. package/src/assets/web-panel/assets/slide-CeP5JCiZ.js +0 -4
  324. package/src/assets/web-panel/assets/statusUtils-DMVoo96t.js +0 -1
  325. package/src/assets/web-panel/assets/transition-DSe9CgZe.js +0 -1
  326. package/src/assets/web-panel/assets/useConfigInject-DCcT0grw.js +0 -2
  327. package/src/assets/web-panel/assets/useFlexGapSupport-BZkoDuF3.js +0 -1
  328. package/src/assets/web-panel/assets/vendor-BVetwVfk.js +0 -1
  329. package/src/assets/web-panel/assets/vnode-Dexy2NN9.js +0 -1
  330. package/src/assets/web-panel/assets/ws-Bv7YH0VB.js +0 -1
  331. package/src/assets/web-panel/assets/zoom-CDfM_sRk.js +0 -4
@@ -11,30 +11,72 @@
11
11
  import fs from "node:fs";
12
12
  import path from "node:path";
13
13
  import chalk from "chalk";
14
+ import { ed25519 as nobleEd25519 } from "@noble/curves/ed25519";
14
15
  import { logger } from "../lib/logger.js";
15
16
  import { bootstrap, shutdown } from "../runtime/bootstrap.js";
16
17
  import { getAllIdentities, getIdentity } from "../lib/did-manager.js";
17
18
  import { CLISkillLoader } from "../lib/skill-loader.js";
19
+ import { getHomeDir } from "../lib/paths.js";
18
20
  import mtcLib from "@chainlesschain/core-mtc";
19
21
 
20
22
  const {
21
- MerkleTree,
22
23
  encodeHashStr,
23
24
  sha256,
24
- leafHash,
25
25
  jcs,
26
26
  LandmarkCache,
27
27
  verify,
28
- SCHEMA_ENVELOPE,
29
- SCHEMA_TREE_HEAD,
30
28
  SCHEMA_LANDMARK,
31
- TREE_HEAD_SIG_PREFIX,
32
29
  ed25519,
30
+ slhDsa,
31
+ assembleBatch,
33
32
  } = mtcLib;
34
33
 
35
34
  const STOPGAP_BANNER = chalk.yellow(
36
- "⚠ STOPGAP — tree-head signed with Ed25519 (will switch to SLH-DSA when @noble/post-quantum lands).",
35
+ "⚠ Tree-head signed with Ed25519 (classical default). Pass --alg slh-dsa-128f for FIPS 205 post-quantum signatures.",
37
36
  );
37
+ const PQC_BANNER = chalk.green(
38
+ "✓ Tree-head signed with SLH-DSA-SHA2-128F (FIPS 205 post-quantum).",
39
+ );
40
+
41
+ /**
42
+ * Resolve --alg flag to a signer module + key sizes for read-back.
43
+ * ed25519 (classical, 32-byte sk) is the default; slh-dsa-128f is opt-in.
44
+ */
45
+ /**
46
+ * Build a verifier that handles either Ed25519 or SLH-DSA tree-head signatures
47
+ * based on the landmark's trust_anchors. Each per-algorithm verifier rejects
48
+ * signatures of the wrong alg via `signatureObj.alg !== ALG`, so chaining
49
+ * them is safe — at most one will accept.
50
+ */
51
+ function makeMultiAlgVerifier(landmark) {
52
+ const ed = ed25519.makeVerifierFromLandmark(landmark);
53
+ const slh = slhDsa.makeVerifierFromLandmark(landmark);
54
+ return (signingInput, signatureObj) =>
55
+ ed(signingInput, signatureObj) || slh(signingInput, signatureObj);
56
+ }
57
+
58
+ function resolveSigner(algFlag) {
59
+ const alg = (algFlag || "ed25519").toLowerCase();
60
+ if (alg === "ed25519") {
61
+ return {
62
+ name: "ed25519",
63
+ signer: ed25519,
64
+ secretKeyLen: 32,
65
+ banner: STOPGAP_BANNER,
66
+ };
67
+ }
68
+ if (alg === "slh-dsa-128f" || alg === "slh-dsa-sha2-128f") {
69
+ return {
70
+ name: "slh-dsa-128f",
71
+ signer: slhDsa,
72
+ secretKeyLen: 64,
73
+ banner: PQC_BANNER,
74
+ };
75
+ }
76
+ throw new Error(
77
+ `Unknown --alg: ${algFlag} (supported: ed25519, slh-dsa-128f)`,
78
+ );
79
+ }
38
80
 
39
81
  function readJsonFile(filePath) {
40
82
  const raw = fs.readFileSync(filePath, "utf-8");
@@ -50,91 +92,362 @@ function writeJsonFile(filePath, obj) {
50
92
  fs.writeFileSync(filePath, JSON.stringify(obj, null, 2), "utf-8");
51
93
  }
52
94
 
53
- function loadOrGenerateKeyPair(secretKeyPath) {
95
+ function loadOrGenerateKeyPair(secretKeyPath, signerInfo) {
96
+ const sig = signerInfo || resolveSigner(null);
54
97
  if (secretKeyPath && fs.existsSync(secretKeyPath)) {
55
98
  const secretKey = Buffer.from(
56
99
  fs.readFileSync(secretKeyPath, "utf-8").trim(),
57
100
  "hex",
58
101
  );
59
- if (secretKey.length !== 32) {
102
+ if (secretKey.length !== sig.secretKeyLen) {
60
103
  throw new Error(
61
- `Secret key file ${secretKeyPath} must contain 32 bytes (64 hex chars)`,
104
+ `Secret key file ${secretKeyPath} must contain ${sig.secretKeyLen} bytes for ${sig.name}`,
62
105
  );
63
106
  }
64
- // Derive publicKey from secretKey via @noble/curves through ed25519.* — use generateKeyPair-style reload
65
- // Simpler: re-derive via a helper. Since lib doesn't expose pubkey-from-secret directly, sign a probe
66
- // and use ed25519.signRaw + ed25519.signTreeHead to recover pubkey isn't possible. Fall back to using
67
- // a fresh keypair if no helper. Rather than add a helper, use @noble/curves directly here.
68
- const { ed25519: ed25519Curve } = require("@noble/curves/ed25519");
69
- const publicKey = Buffer.from(ed25519Curve.getPublicKey(secretKey));
107
+ let publicKey;
108
+ if (sig.name === "ed25519") {
109
+ publicKey = Buffer.from(nobleEd25519.getPublicKey(secretKey));
110
+ } else {
111
+ publicKey = sig.signer.getPublicKey(secretKey);
112
+ }
70
113
  return {
71
114
  secretKey,
72
115
  publicKey,
73
- pubkeyId: ed25519.pubkeyId(publicKey),
116
+ pubkeyId: sig.signer.pubkeyId(publicKey),
74
117
  };
75
118
  }
76
- return ed25519.generateKeyPair();
119
+ return sig.signer.generateKeyPair();
77
120
  }
78
121
 
79
122
  function buildBatch(rawLeaves, opts) {
80
- const namespace = opts.namespace;
81
- const issuer = opts.issuer;
82
- const issuedAt = opts.issuedAt || new Date().toISOString();
83
- const expiresAt =
84
- opts.expiresAt || new Date(Date.now() + 7 * 24 * 3600 * 1000).toISOString();
123
+ // Phase 3.2: federation path takes precedence when --federation is set.
124
+ if (opts.federation) {
125
+ return buildFederatedBatch(rawLeaves, opts);
126
+ }
85
127
 
86
- const keys = loadOrGenerateKeyPair(opts.secretKeyFile);
128
+ const sig = resolveSigner(opts.alg);
129
+ const keys = loadOrGenerateKeyPair(opts.secretKeyFile, sig);
130
+ const { landmark, envelopes, treeHeadId } = assembleBatch(
131
+ rawLeaves,
132
+ keys,
133
+ {
134
+ namespace: opts.namespace,
135
+ issuer: opts.issuer,
136
+ issuedAt: opts.issuedAt,
137
+ expiresAt: opts.expiresAt,
138
+ },
139
+ sig.signer,
140
+ );
141
+ return { landmark, envelopes, treeHeadId, keys, signerInfo: sig };
142
+ }
87
143
 
88
- const leafHashes = rawLeaves.map((l) => leafHash(jcs(l)));
89
- const tree = new MerkleTree(leafHashes);
90
- const root = tree.root();
144
+ /**
145
+ * Federation-mode batch: loads all members of the named federation from
146
+ * the local registry, signs the tree_head with each member's key, and
147
+ * assembles an M-of-N landmark via assembleBatchFederated.
148
+ *
149
+ * @param {Array<object>} rawLeaves
150
+ * @param {{
151
+ * federation: string, // federation id from `cc mtc federation join <id>`
152
+ * threshold?: number, // M (default = N = all members)
153
+ * namespace: string,
154
+ * issuer: string, // federation-level issuer (overrides individual member issuers in tree_head only)
155
+ * issuedAt?: string,
156
+ * expiresAt?: string,
157
+ * }} opts
158
+ */
159
+ function buildFederatedBatch(rawLeaves, opts) {
160
+ const registry = loadFederationRegistry();
161
+ const fed = registry.federations[opts.federation];
162
+ if (!fed) {
163
+ throw new Error(
164
+ `unknown federation "${opts.federation}" — run \`cc mtc federation join ${opts.federation} --member-id <m>\` first`,
165
+ );
166
+ }
167
+ const members = Object.values(fed.members || {});
168
+ if (members.length === 0) {
169
+ throw new Error(`federation "${opts.federation}" has no members`);
170
+ }
91
171
 
92
- const treeHead = {
93
- schema: SCHEMA_TREE_HEAD,
94
- namespace,
95
- tree_size: leafHashes.length,
96
- root_hash: encodeHashStr(root),
97
- issued_at: issuedAt,
98
- expires_at: expiresAt,
99
- issuer,
100
- };
101
- const canonical = jcs(treeHead);
102
- const treeHeadId = encodeHashStr(sha256(canonical));
103
- const signingInput = Buffer.concat([TREE_HEAD_SIG_PREFIX, canonical]);
104
- const signature = ed25519.signTreeHead(signingInput, {
105
- secretKey: keys.secretKey,
106
- publicKey: keys.publicKey,
107
- issuer,
172
+ const threshold = Number.isInteger(opts.threshold)
173
+ ? opts.threshold
174
+ : members.length;
175
+
176
+ // Load each member's signing key from disk
177
+ const signers = members.map((m) => {
178
+ if (!m.key_file || !fs.existsSync(m.key_file)) {
179
+ throw new Error(
180
+ `member "${m.member_id}" key file missing: ${m.key_file}`,
181
+ );
182
+ }
183
+ // Match alg from registry, not from --alg flag (each member is fixed)
184
+ let sigInfo;
185
+ if (m.alg === "Ed25519") {
186
+ sigInfo = resolveSigner("ed25519");
187
+ } else if (m.alg === "SLH-DSA-SHA2-128F") {
188
+ sigInfo = resolveSigner("slh-dsa-128f");
189
+ } else {
190
+ throw new Error(`member "${m.member_id}" has unknown alg: ${m.alg}`);
191
+ }
192
+ const keys = loadOrGenerateKeyPair(m.key_file, sigInfo);
193
+ return {
194
+ secretKey: keys.secretKey,
195
+ publicKey: keys.publicKey,
196
+ signer: sigInfo.signer,
197
+ issuer: m.issuer,
198
+ };
108
199
  });
109
200
 
110
- const landmark = {
111
- schema: SCHEMA_LANDMARK,
112
- namespace: namespace.split("/").slice(0, -1).join("/"),
113
- snapshots: [{ tree_head: treeHead, tree_head_id: treeHeadId, signature }],
114
- trust_anchors: [ed25519.trustAnchorEntry(keys.publicKey, issuer)],
115
- published_at: issuedAt,
116
- publisher_signature: {
117
- alg: "Ed25519",
118
- key_id: issuer + "#key-1",
119
- sig: "TODO-PUBLISHER-SIG",
201
+ const fedSignerInfo = {
202
+ name: "federation",
203
+ threshold,
204
+ members: members.length,
205
+ member_ids: members.map((m) => m.member_id),
206
+ banner: chalk.cyan(
207
+ `✓ Federated tree-head — ${threshold}-of-${members.length} multi-signature (federation: ${opts.federation})`,
208
+ ),
209
+ };
210
+
211
+ const { landmark, envelopes, treeHeadId } = mtcLib.assembleBatchFederated(
212
+ rawLeaves,
213
+ signers,
214
+ {
215
+ namespace: opts.namespace,
216
+ issuer: opts.issuer,
217
+ threshold,
218
+ issuedAt: opts.issuedAt,
219
+ expiresAt: opts.expiresAt,
120
220
  },
221
+ );
222
+
223
+ return {
224
+ landmark,
225
+ envelopes,
226
+ treeHeadId,
227
+ keys: signers[0], // first member's key (publish-skills tries to persist; harmless to surface)
228
+ signerInfo: fedSignerInfo,
121
229
  };
230
+ }
122
231
 
123
- const envelopes = rawLeaves.map((leaf, i) => ({
124
- schema: SCHEMA_ENVELOPE,
125
- namespace,
126
- tree_head_id: treeHeadId,
127
- leaf,
128
- inclusion_proof: {
129
- leaf_index: i,
130
- tree_size: leafHashes.length,
131
- audit_path: tree.prove(i).map((b) => encodeHashStr(b)),
232
+ // ─────────────────────────────────────────────────────────────────────
233
+ // Marketplace publisher daemon (Phase 2 marketplace path)
234
+ // ─────────────────────────────────────────────────────────────────────
235
+
236
+ const PUBLISH_STATE_SCHEMA = "mtc-skill-publish-state/v1";
237
+
238
+ function canonicalSkillsFingerprint(skills) {
239
+ // Sort by id for deterministic fingerprint, then JCS-canonicalize a tuple
240
+ // of (id, version, content_hash). This matches what batch-skills hashes
241
+ // into each leaf, so changing only metadata doesn't trigger churn but
242
+ // bumping a skill's version or body does.
243
+ const sorted = [...skills].sort((a, b) => a.id.localeCompare(b.id));
244
+ const tuples = sorted.map((s) => ({
245
+ id: s.id,
246
+ version: s.version,
247
+ category: s.category,
248
+ activation: s.activation,
249
+ description: s.description,
250
+ }));
251
+ return encodeHashStr(sha256(jcs(tuples)));
252
+ }
253
+
254
+ function loadPublishState(filePath) {
255
+ if (!filePath || !fs.existsSync(filePath)) {
256
+ return {
257
+ schema: PUBLISH_STATE_SCHEMA,
258
+ last_seq: 0,
259
+ last_fingerprint: null,
260
+ last_published_at: null,
261
+ history: [],
262
+ };
263
+ }
264
+ const obj = readJsonFile(filePath);
265
+ if (obj.schema !== PUBLISH_STATE_SCHEMA) {
266
+ throw new Error(`state file ${filePath} has unknown schema: ${obj.schema}`);
267
+ }
268
+ return obj;
269
+ }
270
+
271
+ function savePublishState(filePath, state) {
272
+ // Atomic write: tmp + rename. Avoids the next run reading a truncated state
273
+ // file if the process crashes mid-write (which would silently reset last_seq
274
+ // and start re-publishing batches at 000001).
275
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
276
+ const tmp = `${filePath}.${process.pid}.tmp`;
277
+ fs.writeFileSync(tmp, JSON.stringify(state, null, 2), "utf-8");
278
+ fs.renameSync(tmp, filePath);
279
+ }
280
+
281
+ /**
282
+ * One iteration: load skills, compare fingerprint, optionally build a batch
283
+ * and persist state. Returns a structured result regardless of whether a
284
+ * batch was produced (for json output + tests).
285
+ */
286
+ function publishSkillsOnce(options) {
287
+ const loader = new CLISkillLoader();
288
+ const allSkills = loader.loadAll();
289
+
290
+ let skills;
291
+ if (options.skill && options.skill.length > 0) {
292
+ const want = new Set(options.skill);
293
+ skills = allSkills.filter((s) => want.has(s.id));
294
+ } else {
295
+ skills = allSkills;
296
+ }
297
+
298
+ const state = loadPublishState(options.stateFile);
299
+ if (skills.length === 0) {
300
+ return {
301
+ iteration: "skipped",
302
+ reason: "no skills discovered",
303
+ skills_count: 0,
304
+ last_seq: state.last_seq,
305
+ };
306
+ }
307
+
308
+ const fingerprint = canonicalSkillsFingerprint(skills);
309
+ if (state.last_fingerprint === fingerprint) {
310
+ return {
311
+ iteration: "skipped",
312
+ reason: "fingerprint unchanged",
313
+ skills_count: skills.length,
314
+ last_seq: state.last_seq,
315
+ fingerprint,
316
+ };
317
+ }
318
+
319
+ const nextSeq = state.last_seq + 1;
320
+ const seqStr = String(nextSeq).padStart(6, "0");
321
+ const namespace = `${options.namespacePrefix}/${seqStr}`;
322
+ const batchDir = path.resolve(options.out, seqStr);
323
+
324
+ const rawLeaves = skills.map((s) => ({
325
+ kind: "skill-manifest",
326
+ content_hash: encodeHashStr(
327
+ sha256(
328
+ jcs({
329
+ id: s.id,
330
+ displayName: s.displayName,
331
+ description: s.description,
332
+ version: s.version,
333
+ category: s.category,
334
+ activation: s.activation,
335
+ tags: s.tags,
336
+ }),
337
+ ),
338
+ ),
339
+ issued_at: new Date().toISOString(),
340
+ subject: `skill:cc:${s.id}@${s.version}`,
341
+ metadata: {
342
+ publisher: options.issuer,
343
+ skill_id: s.id,
344
+ version: s.version,
345
+ category: s.category,
132
346
  },
133
347
  }));
134
348
 
135
- return { landmark, envelopes, treeHeadId, keys };
349
+ const { landmark, envelopes, treeHeadId, keys } = buildBatch(rawLeaves, {
350
+ namespace,
351
+ issuer: options.issuer,
352
+ secretKeyFile: options.secretKeyFile,
353
+ alg: options.alg,
354
+ });
355
+
356
+ fs.mkdirSync(batchDir, { recursive: true });
357
+ const landmarkPath = path.join(batchDir, "landmark.json");
358
+ writeJsonFile(landmarkPath, landmark);
359
+ if (options.secretKeyFile && !fs.existsSync(options.secretKeyFile)) {
360
+ fs.mkdirSync(path.dirname(options.secretKeyFile), { recursive: true });
361
+ fs.writeFileSync(options.secretKeyFile, keys.secretKey.toString("hex"), {
362
+ mode: 0o600,
363
+ });
364
+ }
365
+ const envelopePaths = [];
366
+ for (let i = 0; i < envelopes.length; i++) {
367
+ const p = path.join(
368
+ batchDir,
369
+ `envelope-${String(i).padStart(6, "0")}.json`,
370
+ );
371
+ writeJsonFile(p, envelopes[i]);
372
+ envelopePaths.push(p);
373
+ }
374
+
375
+ const publishedAt = new Date().toISOString();
376
+ state.last_seq = nextSeq;
377
+ state.last_fingerprint = fingerprint;
378
+ state.last_published_at = publishedAt;
379
+ state.history.push({
380
+ seq: seqStr,
381
+ namespace,
382
+ tree_head_id: treeHeadId,
383
+ root_hash: landmark.snapshots[0].tree_head.root_hash,
384
+ tree_size: skills.length,
385
+ fingerprint,
386
+ published_at: publishedAt,
387
+ batch_dir: batchDir,
388
+ });
389
+ savePublishState(options.stateFile, state);
390
+
391
+ return {
392
+ iteration: "published",
393
+ seq: seqStr,
394
+ namespace,
395
+ tree_head_id: treeHeadId,
396
+ tree_size: skills.length,
397
+ batch_dir: batchDir,
398
+ landmark_path: landmarkPath,
399
+ envelope_paths: envelopePaths,
400
+ fingerprint,
401
+ };
402
+ }
403
+
404
+ async function publishSkillsLoop(options) {
405
+ const tick = () => {
406
+ try {
407
+ const result = publishSkillsOnce(options);
408
+ if (options.json) {
409
+ console.log(JSON.stringify(result, null, 2));
410
+ } else if (result.iteration === "published") {
411
+ logger.success(
412
+ `[seq ${result.seq}] published ${result.tree_size} skill(s) → ${result.batch_dir}`,
413
+ );
414
+ logger.log(` ${chalk.bold("Tree head ID:")} ${result.tree_head_id}`);
415
+ } else {
416
+ logger.info(`skipped: ${result.reason}`);
417
+ }
418
+ } catch (err) {
419
+ logger.error(`iteration failed: ${err.message}`);
420
+ if (options.once) throw err;
421
+ }
422
+ };
423
+
424
+ if (options.once) {
425
+ tick();
426
+ return;
427
+ }
428
+
429
+ tick();
430
+ const ms = Math.max(1, options.interval) * 1000;
431
+ const timer = setInterval(tick, ms);
432
+ const cleanup = () => {
433
+ clearInterval(timer);
434
+ process.exit(0);
435
+ };
436
+ process.once("SIGINT", cleanup);
437
+ process.once("SIGTERM", cleanup);
438
+ // Daemon: never resolve.
439
+ await new Promise(() => {});
136
440
  }
137
441
 
442
+ // Export internals for unit tests
443
+ export const _publishInternals = {
444
+ publishSkillsOnce,
445
+ loadPublishState,
446
+ savePublishState,
447
+ canonicalSkillsFingerprint,
448
+ PUBLISH_STATE_SCHEMA,
449
+ };
450
+
138
451
  export function registerMtcCommand(program) {
139
452
  const mtc = program
140
453
  .command("mtc")
@@ -155,7 +468,21 @@ export function registerMtcCommand(program) {
155
468
  .option("--expires-at <iso>", "Override expires_at timestamp")
156
469
  .option(
157
470
  "--secret-key-file <path>",
158
- "Reuse Ed25519 secret key from this hex file (creates one if missing)",
471
+ "Reuse secret key from hex file (creates one if missing); 32 B for ed25519, 64 B for slh-dsa-128f",
472
+ )
473
+ .option(
474
+ "--alg <name>",
475
+ "Signing algorithm: ed25519 (default, classical) | slh-dsa-128f (FIPS 205 post-quantum)",
476
+ "ed25519",
477
+ )
478
+ .option(
479
+ "--federation <id>",
480
+ "Use federation M-of-N multi-sig (overrides --alg / --secret-key-file; signers come from `cc mtc federation join` registry)",
481
+ )
482
+ .option(
483
+ "--threshold <n>",
484
+ "Federation threshold M (default: N = all members)",
485
+ (v) => parseInt(v, 10),
159
486
  )
160
487
  .option("--json", "Print JSON summary instead of human output")
161
488
  .action(async (inputPath, options) => {
@@ -165,16 +492,17 @@ export function registerMtcCommand(program) {
165
492
  throw new Error("Input must be a non-empty JSON array of leaves");
166
493
  }
167
494
 
168
- const { landmark, envelopes, treeHeadId, keys } = buildBatch(
169
- rawLeaves,
170
- {
495
+ const { landmark, envelopes, treeHeadId, keys, signerInfo } =
496
+ buildBatch(rawLeaves, {
171
497
  namespace: options.namespace,
172
498
  issuer: options.issuer,
173
499
  issuedAt: options.issuedAt,
174
500
  expiresAt: options.expiresAt,
175
501
  secretKeyFile: options.secretKeyFile,
176
- },
177
- );
502
+ alg: options.alg,
503
+ federation: options.federation,
504
+ threshold: options.threshold,
505
+ });
178
506
 
179
507
  const outDir = path.resolve(options.out);
180
508
  const landmarkPath = path.join(outDir, "landmark.json");
@@ -218,7 +546,7 @@ export function registerMtcCommand(program) {
218
546
  ),
219
547
  );
220
548
  } else {
221
- logger.log(STOPGAP_BANNER);
549
+ logger.log(signerInfo ? signerInfo.banner : STOPGAP_BANNER);
222
550
  logger.success("Batch built");
223
551
  logger.log(` ${chalk.bold("Namespace:")} ${options.namespace}`);
224
552
  logger.log(` ${chalk.bold("Tree size:")} ${rawLeaves.length}`);
@@ -253,7 +581,7 @@ export function registerMtcCommand(program) {
253
581
  const landmark = readJsonFile(options.landmark);
254
582
 
255
583
  const cache = new LandmarkCache({
256
- signatureVerifier: ed25519.makeVerifierFromLandmark(landmark),
584
+ signatureVerifier: makeMultiAlgVerifier(landmark),
257
585
  });
258
586
  cache.ingest(landmark);
259
587
 
@@ -266,7 +594,7 @@ export function registerMtcCommand(program) {
266
594
  return;
267
595
  }
268
596
  if (result.ok) {
269
- logger.log(STOPGAP_BANNER);
597
+ logger.log(signerInfo ? signerInfo.banner : STOPGAP_BANNER);
270
598
  logger.success(`Envelope verified`);
271
599
  logger.log(
272
600
  ` ${chalk.bold("Subject:")} ${result.leaf.subject || "(no subject)"}`,
@@ -375,7 +703,21 @@ export function registerMtcCommand(program) {
375
703
  .option("--expires-at <iso>", "Override expires_at timestamp")
376
704
  .option(
377
705
  "--secret-key-file <path>",
378
- "Reuse Ed25519 secret key from this hex file (creates one if missing)",
706
+ "Reuse secret key from hex file (creates one if missing); 32 B for ed25519, 64 B for slh-dsa-128f",
707
+ )
708
+ .option(
709
+ "--alg <name>",
710
+ "Signing algorithm: ed25519 (default, classical) | slh-dsa-128f (FIPS 205 post-quantum)",
711
+ "ed25519",
712
+ )
713
+ .option(
714
+ "--federation <id>",
715
+ "Use federation M-of-N multi-sig (overrides --alg / --secret-key-file; signers come from `cc mtc federation join` registry)",
716
+ )
717
+ .option(
718
+ "--threshold <n>",
719
+ "Federation threshold M (default: N = all members)",
720
+ (v) => parseInt(v, 10),
379
721
  )
380
722
  .option("--json", "Print JSON summary instead of human output")
381
723
  .action(async (options) => {
@@ -417,16 +759,17 @@ export function registerMtcCommand(program) {
417
759
  };
418
760
  });
419
761
 
420
- const { landmark, envelopes, treeHeadId, keys } = buildBatch(
421
- rawLeaves,
422
- {
762
+ const { landmark, envelopes, treeHeadId, keys, signerInfo } =
763
+ buildBatch(rawLeaves, {
423
764
  namespace: options.namespace,
424
765
  issuer: options.issuer,
425
766
  issuedAt: options.issuedAt,
426
767
  expiresAt: options.expiresAt,
427
768
  secretKeyFile: options.secretKeyFile,
428
- },
429
- );
769
+ alg: options.alg,
770
+ federation: options.federation,
771
+ threshold: options.threshold,
772
+ });
430
773
 
431
774
  const outDir = path.resolve(options.out);
432
775
  const landmarkPath = path.join(outDir, "landmark.json");
@@ -472,7 +815,7 @@ export function registerMtcCommand(program) {
472
815
  ),
473
816
  );
474
817
  } else {
475
- logger.log(STOPGAP_BANNER);
818
+ logger.log(signerInfo ? signerInfo.banner : STOPGAP_BANNER);
476
819
  logger.success(`Batched ${rawLeaves.length} DID(s)`);
477
820
  logger.log(` ${chalk.bold("Namespace:")} ${options.namespace}`);
478
821
  logger.log(` ${chalk.bold("Tree size:")} ${rawLeaves.length}`);
@@ -514,7 +857,21 @@ export function registerMtcCommand(program) {
514
857
  .option("--expires-at <iso>", "Override expires_at timestamp")
515
858
  .option(
516
859
  "--secret-key-file <path>",
517
- "Reuse Ed25519 secret key from this hex file (creates one if missing)",
860
+ "Reuse secret key from hex file (creates one if missing); 32 B for ed25519, 64 B for slh-dsa-128f",
861
+ )
862
+ .option(
863
+ "--alg <name>",
864
+ "Signing algorithm: ed25519 (default, classical) | slh-dsa-128f (FIPS 205 post-quantum)",
865
+ "ed25519",
866
+ )
867
+ .option(
868
+ "--federation <id>",
869
+ "Use federation M-of-N multi-sig (overrides --alg / --secret-key-file; signers come from `cc mtc federation join` registry)",
870
+ )
871
+ .option(
872
+ "--threshold <n>",
873
+ "Federation threshold M (default: N = all members)",
874
+ (v) => parseInt(v, 10),
518
875
  )
519
876
  .option("--json", "Print JSON summary instead of human output")
520
877
  .action(async (options) => {
@@ -563,16 +920,17 @@ export function registerMtcCommand(program) {
563
920
  };
564
921
  });
565
922
 
566
- const { landmark, envelopes, treeHeadId, keys } = buildBatch(
567
- rawLeaves,
568
- {
923
+ const { landmark, envelopes, treeHeadId, keys, signerInfo } =
924
+ buildBatch(rawLeaves, {
569
925
  namespace: options.namespace,
570
926
  issuer: options.issuer,
571
927
  issuedAt: options.issuedAt,
572
928
  expiresAt: options.expiresAt,
573
929
  secretKeyFile: options.secretKeyFile,
574
- },
575
- );
930
+ alg: options.alg,
931
+ federation: options.federation,
932
+ threshold: options.threshold,
933
+ });
576
934
 
577
935
  const outDir = path.resolve(options.out);
578
936
  const landmarkPath = path.join(outDir, "landmark.json");
@@ -618,7 +976,7 @@ export function registerMtcCommand(program) {
618
976
  ),
619
977
  );
620
978
  } else {
621
- logger.log(STOPGAP_BANNER);
979
+ logger.log(signerInfo ? signerInfo.banner : STOPGAP_BANNER);
622
980
  logger.success(`Batched ${rawLeaves.length} skill(s)`);
623
981
  logger.log(` ${chalk.bold("Namespace:")} ${options.namespace}`);
624
982
  logger.log(` ${chalk.bold("Tree size:")} ${rawLeaves.length}`);
@@ -641,6 +999,154 @@ export function registerMtcCommand(program) {
641
999
  }
642
1000
  });
643
1001
 
1002
+ // mtc publish-skills — marketplace publisher daemon
1003
+ // Periodically scans CLISkillLoader, detects deltas via a fingerprint, and
1004
+ // when the skill set changes auto-closes a new batch (assembleBatch) into
1005
+ // <out>/<seq>/. Stateful via a JSON state file so re-runs skip unchanged sets.
1006
+ //
1007
+ // Phase 2 marketplace path — does NOT depend on the audit Q-COMP blockers.
1008
+ mtc
1009
+ .command("publish-skills")
1010
+ .description(
1011
+ "Marketplace publisher daemon: detect skill deltas + auto-close batches",
1012
+ )
1013
+ .requiredOption(
1014
+ "--namespace-prefix <prefix>",
1015
+ "Namespace prefix; seq is auto-appended (e.g. mtc/v1/skill)",
1016
+ )
1017
+ .requiredOption("--issuer <issuer>", "MTCA issuer string")
1018
+ .requiredOption(
1019
+ "--out <dir>",
1020
+ "Output root directory (each batch lands in <out>/<seq>/)",
1021
+ )
1022
+ .requiredOption(
1023
+ "--state-file <path>",
1024
+ "State JSON file tracking last_seq + fingerprint",
1025
+ )
1026
+ .option(
1027
+ "--secret-key-file <path>",
1028
+ "Reuse secret key from hex file (creates one if missing); 32 B for ed25519, 64 B for slh-dsa-128f",
1029
+ )
1030
+ .option(
1031
+ "--alg <name>",
1032
+ "Signing algorithm: ed25519 (default, classical) | slh-dsa-128f (FIPS 205 post-quantum)",
1033
+ "ed25519",
1034
+ )
1035
+ .option(
1036
+ "--federation <id>",
1037
+ "Use federation M-of-N multi-sig (overrides --alg / --secret-key-file; signers come from `cc mtc federation join` registry)",
1038
+ )
1039
+ .option(
1040
+ "--threshold <n>",
1041
+ "Federation threshold M (default: N = all members)",
1042
+ (v) => parseInt(v, 10),
1043
+ )
1044
+ .option(
1045
+ "--interval <seconds>",
1046
+ "Loop interval (default: 600 = 10min, ignored if --once)",
1047
+ (v) => parseInt(v, 10),
1048
+ 600,
1049
+ )
1050
+ .option("--once", "Run a single iteration and exit (test/CI use)")
1051
+ .option(
1052
+ "--skill <id>",
1053
+ "Restrict to specific skill ids (repeatable)",
1054
+ (v, prev) => [...(prev || []), v],
1055
+ )
1056
+ .option("--json", "Print JSON summary on each iteration")
1057
+ .action(async (options) => {
1058
+ try {
1059
+ await publishSkillsLoop(options);
1060
+ } catch (err) {
1061
+ logger.error(`mtc publish-skills failed: ${err.message}`);
1062
+ process.exit(1);
1063
+ }
1064
+ });
1065
+
1066
+ // mtc publish-status — read-only inspector for publish-skills state file.
1067
+ // Used by web-panel (browser can't read the filesystem directly; this gives
1068
+ // it a CLI-bridge-friendly query path without exposing the daemon machinery).
1069
+ mtc
1070
+ .command("publish-status <state-file>")
1071
+ .description(
1072
+ "Read a publish-skills state file and print its current state + recent history",
1073
+ )
1074
+ .option(
1075
+ "--limit <n>",
1076
+ "Limit history entries (default: 20, latest first)",
1077
+ (v) => parseInt(v, 10),
1078
+ 20,
1079
+ )
1080
+ .option("--json", "Output JSON (default: human)")
1081
+ .action((stateFile, options) => {
1082
+ try {
1083
+ if (!fs.existsSync(stateFile)) {
1084
+ if (options.json) {
1085
+ console.log(
1086
+ JSON.stringify(
1087
+ { ok: true, exists: false, state_file: stateFile },
1088
+ null,
1089
+ 2,
1090
+ ),
1091
+ );
1092
+ } else {
1093
+ logger.warn(`state file not found: ${stateFile}`);
1094
+ }
1095
+ return;
1096
+ }
1097
+ const state = loadPublishState(stateFile);
1098
+ const history = Array.isArray(state.history) ? state.history : [];
1099
+ const limited = history.slice().reverse().slice(0, options.limit);
1100
+ if (options.json) {
1101
+ console.log(
1102
+ JSON.stringify(
1103
+ {
1104
+ ok: true,
1105
+ exists: true,
1106
+ state_file: stateFile,
1107
+ last_seq: state.last_seq,
1108
+ last_fingerprint: state.last_fingerprint,
1109
+ last_published_at: state.last_published_at,
1110
+ history_count: history.length,
1111
+ history: limited,
1112
+ },
1113
+ null,
1114
+ 2,
1115
+ ),
1116
+ );
1117
+ return;
1118
+ }
1119
+ logger.log(chalk.bold(`Publish state: ${stateFile}`));
1120
+ logger.log(` ${chalk.bold("Last seq:")} ${state.last_seq}`);
1121
+ logger.log(
1122
+ ` ${chalk.bold("Last published:")} ${state.last_published_at || "(never)"}`,
1123
+ );
1124
+ logger.log(` ${chalk.bold("History entries:")} ${history.length}`);
1125
+ if (limited.length > 0) {
1126
+ logger.log("");
1127
+ logger.log(chalk.bold(`Recent history (latest ${limited.length}):`));
1128
+ for (const h of limited) {
1129
+ logger.log(
1130
+ ` ${chalk.cyan(h.seq)} ${h.namespace} size=${h.tree_size} ${chalk.gray(h.published_at)}`,
1131
+ );
1132
+ }
1133
+ }
1134
+ } catch (err) {
1135
+ if (options.json) {
1136
+ console.log(
1137
+ JSON.stringify(
1138
+ { ok: false, error: err.message, state_file: stateFile },
1139
+ null,
1140
+ 2,
1141
+ ),
1142
+ );
1143
+ } else {
1144
+ logger.error(`mtc publish-status failed: ${err.message}`);
1145
+ }
1146
+ process.exit(1);
1147
+ }
1148
+ });
1149
+
644
1150
  // mtc serve — verifier daemon: subscribe to a transport, persist + verify
645
1151
  mtc
646
1152
  .command("serve")
@@ -734,7 +1240,7 @@ export function registerMtcCommand(program) {
734
1240
  // Lazy-init cache from first landmark's trust_anchors
735
1241
  if (!cache) {
736
1242
  cache = new LandmarkCache({
737
- signatureVerifier: ed25519.makeVerifierFromLandmark(landmark),
1243
+ signatureVerifier: makeMultiAlgVerifier(landmark),
738
1244
  persistDir: options.cacheDir,
739
1245
  });
740
1246
  if (options.cacheDir) {
@@ -783,4 +1289,813 @@ export function registerMtcCommand(program) {
783
1289
  process.exit(1);
784
1290
  }
785
1291
  });
1292
+
1293
+ // ─────────────────────────────────────────────────────────────────────
1294
+ // Phase 3 federation MTCA commands
1295
+ // M-of-N multi-sig is implemented in core-mtc/lib/batch.js;
1296
+ // federation member tracking lives in ~/.chainlesschain/federation/
1297
+ // members.json (one entry per joined federation, keyed by federation id).
1298
+ // ─────────────────────────────────────────────────────────────────────
1299
+ registerFederationCommands(mtc);
1300
+ }
1301
+
1302
+ // ─────────────────────────────────────────────────────────────────────────
1303
+ // Federation member registry helpers (Phase 3.1)
1304
+ // ─────────────────────────────────────────────────────────────────────────
1305
+
1306
+ const FEDERATION_REGISTRY_SCHEMA = "mtc-federation-registry/v1";
1307
+
1308
+ function getFederationDir() {
1309
+ const home = getHomeDir();
1310
+ return path.join(home, "federation");
786
1311
  }
1312
+
1313
+ function getFederationRegistryPath() {
1314
+ return path.join(getFederationDir(), "members.json");
1315
+ }
1316
+
1317
+ function loadFederationRegistry() {
1318
+ const file = getFederationRegistryPath();
1319
+ if (!fs.existsSync(file)) {
1320
+ return { schema: FEDERATION_REGISTRY_SCHEMA, federations: {} };
1321
+ }
1322
+ const obj = readJsonFile(file);
1323
+ if (obj.schema !== FEDERATION_REGISTRY_SCHEMA) {
1324
+ throw new Error(
1325
+ `federation registry has unknown schema: ${obj.schema} (expected ${FEDERATION_REGISTRY_SCHEMA})`,
1326
+ );
1327
+ }
1328
+ if (!obj.federations || typeof obj.federations !== "object") {
1329
+ obj.federations = {};
1330
+ }
1331
+ return obj;
1332
+ }
1333
+
1334
+ function saveFederationRegistry(registry) {
1335
+ const file = getFederationRegistryPath();
1336
+ fs.mkdirSync(path.dirname(file), { recursive: true });
1337
+ // Atomic write to survive crash mid-write
1338
+ const tmp = `${file}.${process.pid}.tmp`;
1339
+ fs.writeFileSync(tmp, JSON.stringify(registry, null, 2), "utf-8");
1340
+ fs.renameSync(tmp, file);
1341
+ }
1342
+
1343
+ function registerFederationCommands(mtc) {
1344
+ const fed = mtc
1345
+ .command("federation")
1346
+ .description("Phase 3 federation MTCA — manage M-of-N member registry");
1347
+
1348
+ // mtc federation join <federation-id>
1349
+ fed
1350
+ .command("join <federation-id>")
1351
+ .description(
1352
+ "Join a federation: generate a member keypair (or reuse existing) and register it locally",
1353
+ )
1354
+ .requiredOption(
1355
+ "--member-id <id>",
1356
+ "Local member identifier within the federation",
1357
+ )
1358
+ .option(
1359
+ "--alg <name>",
1360
+ "Signing algorithm: ed25519 (default) | slh-dsa-128f",
1361
+ "ed25519",
1362
+ )
1363
+ .option(
1364
+ "--issuer <issuer>",
1365
+ "Member-level issuer string (default: mtca:cc:<federation-id>:<member-id>)",
1366
+ )
1367
+ .option("--key-file <path>", "Reuse existing secret key from hex file")
1368
+ .option("--json", "Print JSON summary")
1369
+ .action((federationId, options) => {
1370
+ try {
1371
+ const sig = resolveSigner(options.alg);
1372
+ const issuer =
1373
+ options.issuer || `mtca:cc:${federationId}:${options.memberId}`;
1374
+ const registry = loadFederationRegistry();
1375
+ const fedEntry = registry.federations[federationId] || {
1376
+ federation_id: federationId,
1377
+ members: {},
1378
+ joined_at: new Date().toISOString(),
1379
+ };
1380
+ if (fedEntry.members[options.memberId]) {
1381
+ throw new Error(
1382
+ `member "${options.memberId}" already registered in federation "${federationId}" — leave first to rejoin`,
1383
+ );
1384
+ }
1385
+
1386
+ // Generate or load the keypair
1387
+ let keys;
1388
+ if (options.keyFile && fs.existsSync(options.keyFile)) {
1389
+ keys = loadOrGenerateKeyPair(options.keyFile, sig);
1390
+ } else {
1391
+ keys = sig.signer.generateKeyPair();
1392
+ }
1393
+
1394
+ // Persist the member's secret key under federation/keys/.
1395
+ // Race-safe: 'wx' fails if another concurrent join already won, in
1396
+ // which case we stick with that file rather than overwrite. Member
1397
+ // re-join is blocked at the registry level above, so this only
1398
+ // guards against simultaneous-join racing the same path.
1399
+ const keysDir = path.join(getFederationDir(), "keys");
1400
+ fs.mkdirSync(keysDir, { recursive: true });
1401
+ const keyPath = path.join(
1402
+ keysDir,
1403
+ `${federationId}.${options.memberId}.hex`,
1404
+ );
1405
+ try {
1406
+ fs.writeFileSync(keyPath, keys.secretKey.toString("hex"), {
1407
+ mode: 0o600,
1408
+ flag: "wx",
1409
+ });
1410
+ } catch (err) {
1411
+ if (err.code !== "EEXIST") throw err;
1412
+ // Concurrent join wrote the file first — re-load from disk and
1413
+ // align our keys so registry + key file match.
1414
+ keys = loadOrGenerateKeyPair(keyPath, sig);
1415
+ }
1416
+
1417
+ const trustAnchor = sig.signer.trustAnchorEntry(keys.publicKey, issuer);
1418
+ fedEntry.members[options.memberId] = {
1419
+ member_id: options.memberId,
1420
+ issuer,
1421
+ alg: sig.signer.ALG,
1422
+ pubkey_id: trustAnchor.pubkey_id,
1423
+ pubkey_jwk: trustAnchor.pubkey_jwk,
1424
+ key_file: keyPath,
1425
+ joined_at: new Date().toISOString(),
1426
+ };
1427
+ registry.federations[federationId] = fedEntry;
1428
+ saveFederationRegistry(registry);
1429
+
1430
+ if (options.json) {
1431
+ console.log(
1432
+ JSON.stringify(
1433
+ {
1434
+ ok: true,
1435
+ federation_id: federationId,
1436
+ member_id: options.memberId,
1437
+ issuer,
1438
+ alg: sig.signer.ALG,
1439
+ pubkey_id: trustAnchor.pubkey_id,
1440
+ key_file: keyPath,
1441
+ },
1442
+ null,
1443
+ 2,
1444
+ ),
1445
+ );
1446
+ } else {
1447
+ logger.success(
1448
+ `joined federation "${federationId}" as "${options.memberId}"`,
1449
+ );
1450
+ logger.log(` ${chalk.bold("Issuer:")} ${issuer}`);
1451
+ logger.log(` ${chalk.bold("Algorithm:")} ${sig.signer.ALG}`);
1452
+ logger.log(` ${chalk.bold("Pubkey id:")} ${trustAnchor.pubkey_id}`);
1453
+ logger.log(` ${chalk.bold("Key file:")} ${chalk.cyan(keyPath)}`);
1454
+ }
1455
+ } catch (err) {
1456
+ logger.error(`mtc federation join failed: ${err.message}`);
1457
+ process.exit(1);
1458
+ }
1459
+ });
1460
+
1461
+ // mtc federation leave <federation-id> --member-id <id>
1462
+ fed
1463
+ .command("leave <federation-id>")
1464
+ .description(
1465
+ "Leave a federation: remove the member entry from the local registry",
1466
+ )
1467
+ .requiredOption("--member-id <id>", "Member id to remove")
1468
+ .option(
1469
+ "--keep-key",
1470
+ "Keep the secret key file on disk (default: removes the key file as well)",
1471
+ )
1472
+ .option("--json", "Print JSON summary")
1473
+ .action((federationId, options) => {
1474
+ try {
1475
+ const registry = loadFederationRegistry();
1476
+ const fedEntry = registry.federations[federationId];
1477
+ if (!fedEntry || !fedEntry.members[options.memberId]) {
1478
+ throw new Error(
1479
+ `member "${options.memberId}" not found in federation "${federationId}"`,
1480
+ );
1481
+ }
1482
+ const member = fedEntry.members[options.memberId];
1483
+ delete fedEntry.members[options.memberId];
1484
+ if (Object.keys(fedEntry.members).length === 0) {
1485
+ delete registry.federations[federationId];
1486
+ }
1487
+ saveFederationRegistry(registry);
1488
+
1489
+ if (
1490
+ !options.keepKey &&
1491
+ member.key_file &&
1492
+ fs.existsSync(member.key_file)
1493
+ ) {
1494
+ try {
1495
+ fs.unlinkSync(member.key_file);
1496
+ } catch (_err) {
1497
+ /* non-fatal */
1498
+ }
1499
+ }
1500
+
1501
+ if (options.json) {
1502
+ console.log(
1503
+ JSON.stringify(
1504
+ {
1505
+ ok: true,
1506
+ federation_id: federationId,
1507
+ member_id: options.memberId,
1508
+ key_file_removed: !options.keepKey,
1509
+ },
1510
+ null,
1511
+ 2,
1512
+ ),
1513
+ );
1514
+ } else {
1515
+ logger.success(
1516
+ `left federation "${federationId}" — member "${options.memberId}" removed${
1517
+ options.keepKey ? "" : " (key file deleted)"
1518
+ }`,
1519
+ );
1520
+ }
1521
+ } catch (err) {
1522
+ logger.error(`mtc federation leave failed: ${err.message}`);
1523
+ process.exit(1);
1524
+ }
1525
+ });
1526
+
1527
+ // mtc federation status [federation-id]
1528
+ fed
1529
+ .command("status [federation-id]")
1530
+ .description("Show registered federations and their members")
1531
+ .option("--json", "Output JSON")
1532
+ .action((federationId, options) => {
1533
+ try {
1534
+ const registry = loadFederationRegistry();
1535
+ const data = federationId
1536
+ ? { [federationId]: registry.federations[federationId] || null }
1537
+ : registry.federations;
1538
+
1539
+ if (options.json) {
1540
+ console.log(JSON.stringify({ ok: true, federations: data }, null, 2));
1541
+ return;
1542
+ }
1543
+
1544
+ const ids = Object.keys(data).filter((k) => data[k]);
1545
+ if (ids.length === 0) {
1546
+ logger.info(
1547
+ "no federations registered (run `cc mtc federation join <id> --member-id <m>`)",
1548
+ );
1549
+ return;
1550
+ }
1551
+
1552
+ for (const id of ids) {
1553
+ const f = data[id];
1554
+ const memberCount = Object.keys(f.members || {}).length;
1555
+ logger.log(chalk.bold(`Federation: ${chalk.cyan(id)}`));
1556
+ logger.log(` ${chalk.bold("Joined at:")} ${f.joined_at || "—"}`);
1557
+ logger.log(` ${chalk.bold("Members:")} ${memberCount}`);
1558
+ for (const m of Object.values(f.members || {})) {
1559
+ logger.log(
1560
+ ` · ${chalk.green(m.member_id)} (${m.alg}) ${chalk.gray(m.pubkey_id.slice(0, 18) + "…")}`,
1561
+ );
1562
+ logger.log(` issuer: ${m.issuer}`);
1563
+ logger.log(` key: ${chalk.gray(m.key_file)}`);
1564
+ }
1565
+ }
1566
+ } catch (err) {
1567
+ logger.error(`mtc federation status failed: ${err.message}`);
1568
+ process.exit(1);
1569
+ }
1570
+ });
1571
+
1572
+ // ─────────────────────────────────────────────────────────────────────
1573
+ // Phase 3.3 — federation discovery via filesystem drop-zone.
1574
+ // Each member periodically writes a self-signed announce to a shared
1575
+ // directory (NFS / SMB / Syncthing / USB stick). Other nodes scan the
1576
+ // dir + ingest valid announces into a TTL-evicting roster cache.
1577
+ //
1578
+ // Production note: real libp2p gossipsub-based discovery (auto-announce
1579
+ // on a pubsub topic) is the natural next layer — the announce schema +
1580
+ // verify + cache are transport-agnostic, so wiring gossipsub is purely
1581
+ // a delivery question. Filesystem mode covers LAN / shared-fs / offline
1582
+ // use cases without any p2p network code.
1583
+ // ─────────────────────────────────────────────────────────────────────
1584
+ fed
1585
+ .command("discover <federation-id>")
1586
+ .description(
1587
+ "Subscribe to federation announces via filesystem drop-zone or libp2p gossipsub",
1588
+ )
1589
+ .option(
1590
+ "--transport <kind>",
1591
+ "Transport: filesystem (default, --drop-zone required) | libp2p (--listen + --connect)",
1592
+ "filesystem",
1593
+ )
1594
+ .option(
1595
+ "--drop-zone <dir>",
1596
+ "[filesystem] Shared directory all federation members read+write to (NFS / Syncthing / SMB)",
1597
+ )
1598
+ .option(
1599
+ "--listen <multiaddr>",
1600
+ "[libp2p] Listen address (default: /ip4/127.0.0.1/tcp/0)",
1601
+ )
1602
+ .option(
1603
+ "--connect <multiaddr>",
1604
+ "[libp2p] Dial this peer on startup (repeatable)",
1605
+ (v, prev) => [...(prev || []), v],
1606
+ )
1607
+ .option(
1608
+ "--member-id <id>",
1609
+ "If joined as this member, also publish a self-announce (omit = listen-only mode)",
1610
+ )
1611
+ .option(
1612
+ "--ttl <seconds>",
1613
+ "Announce TTL (default 600 = 10 min); a re-announce fires at TTL/3",
1614
+ (v) => parseInt(v, 10),
1615
+ 600,
1616
+ )
1617
+ .option("--once", "Announce once + scan once + exit (test/CI use)")
1618
+ .option(
1619
+ "--cache-dir <dir>",
1620
+ "Persist accepted announces to this dir for restart resume",
1621
+ )
1622
+ .option(
1623
+ "--scan-interval <seconds>",
1624
+ "[filesystem] Drop-zone poll interval (default 30)",
1625
+ (v) => parseInt(v, 10),
1626
+ 30,
1627
+ )
1628
+ .option(
1629
+ "--mesh-wait-ms <n>",
1630
+ "[libp2p] Mesh formation wait before announce (default 1500)",
1631
+ (v) => parseInt(v, 10),
1632
+ 1500,
1633
+ )
1634
+ .option("--json", "Print JSON status snapshot (used with --once)")
1635
+ .action(async (federationId, options) => {
1636
+ try {
1637
+ await runFederationDiscover(federationId, options);
1638
+ } catch (err) {
1639
+ logger.error(`mtc federation discover failed: ${err.message}`);
1640
+ process.exit(1);
1641
+ }
1642
+ });
1643
+ }
1644
+
1645
+ // ─────────────────────────────────────────────────────────────────────────
1646
+ // Federation discover daemon (Phase 3.3)
1647
+ // ─────────────────────────────────────────────────────────────────────────
1648
+
1649
+ function getDiscoverAnnouncesDir(dropZone, federationId) {
1650
+ return path.join(dropZone, "federation-announces", federationId);
1651
+ }
1652
+
1653
+ function getDiscoverFilename(announce) {
1654
+ // pubkey_id is "sha256:base64url" — replace : for cross-platform safety
1655
+ const safe = announce.pubkey_id.replace(/[^a-zA-Z0-9_-]/g, "_");
1656
+ return `${safe}.json`;
1657
+ }
1658
+
1659
+ function publishAnnounce(dropZone, announce) {
1660
+ const dir = getDiscoverAnnouncesDir(dropZone, announce.federation_id);
1661
+ fs.mkdirSync(dir, { recursive: true });
1662
+ const filename = getDiscoverFilename(announce);
1663
+ const target = path.join(dir, filename);
1664
+ // Atomic write: tmp + rename
1665
+ const tmp = `${target}.${process.pid}.tmp`;
1666
+ fs.writeFileSync(tmp, JSON.stringify(announce, null, 2), "utf-8");
1667
+ fs.renameSync(tmp, target);
1668
+ return target;
1669
+ }
1670
+
1671
+ function scanDropZone(dropZone, federationId) {
1672
+ const dir = getDiscoverAnnouncesDir(dropZone, federationId);
1673
+ if (!fs.existsSync(dir)) return [];
1674
+ return fs
1675
+ .readdirSync(dir)
1676
+ .filter((n) => n.endsWith(".json"))
1677
+ .map((n) => {
1678
+ const file = path.join(dir, n);
1679
+ try {
1680
+ return { file, announce: JSON.parse(fs.readFileSync(file, "utf-8")) };
1681
+ } catch (err) {
1682
+ return { file, error: err.message };
1683
+ }
1684
+ });
1685
+ }
1686
+
1687
+ /**
1688
+ * Helper: load member key + return {announceBuilder, member} for self-announce.
1689
+ * Used by both filesystem and libp2p paths.
1690
+ */
1691
+ function loadFederationMemberForAnnounce(federationId, memberId, ttlSeconds) {
1692
+ const registry = loadFederationRegistry();
1693
+ const fedEntry = registry.federations[federationId];
1694
+ if (!fedEntry || !fedEntry.members[memberId]) {
1695
+ throw new Error(
1696
+ `not joined as "${memberId}" in federation "${federationId}" — run \`cc mtc federation join ${federationId} --member-id ${memberId}\` first`,
1697
+ );
1698
+ }
1699
+ const member = fedEntry.members[memberId];
1700
+ if (!member.key_file || !fs.existsSync(member.key_file)) {
1701
+ throw new Error(`member key file missing: ${member.key_file}`);
1702
+ }
1703
+ let signerInfo;
1704
+ if (member.alg === "Ed25519") signerInfo = resolveSigner("ed25519");
1705
+ else if (member.alg === "SLH-DSA-SHA2-128F")
1706
+ signerInfo = resolveSigner("slh-dsa-128f");
1707
+ else throw new Error(`unknown member alg: ${member.alg}`);
1708
+ const keys = loadOrGenerateKeyPair(member.key_file, signerInfo);
1709
+
1710
+ return {
1711
+ member,
1712
+ buildAnnounce: () =>
1713
+ mtcLib.createMemberAnnounce({
1714
+ federationId,
1715
+ memberId,
1716
+ issuer: member.issuer,
1717
+ secretKey: keys.secretKey,
1718
+ publicKey: keys.publicKey,
1719
+ signer: signerInfo.signer,
1720
+ ttlSeconds,
1721
+ }),
1722
+ };
1723
+ }
1724
+
1725
+ async function runFederationDiscover(federationId, options) {
1726
+ const transport = (options.transport || "filesystem").toLowerCase();
1727
+ if (transport === "libp2p") {
1728
+ return runFederationDiscoverLibp2p(federationId, options);
1729
+ }
1730
+ if (transport !== "filesystem") {
1731
+ throw new Error(
1732
+ `Unknown --transport: ${options.transport} (supported: filesystem, libp2p)`,
1733
+ );
1734
+ }
1735
+ if (!options.dropZone) {
1736
+ throw new Error("--drop-zone is required when --transport=filesystem");
1737
+ }
1738
+ return runFederationDiscoverFilesystem(federationId, options);
1739
+ }
1740
+
1741
+ async function runFederationDiscoverFilesystem(federationId, options) {
1742
+ const FederationAnnounceCache = mtcLib.FederationAnnounceCache;
1743
+
1744
+ const cache = new FederationAnnounceCache({
1745
+ persistDir: options.cacheDir,
1746
+ });
1747
+
1748
+ // Build self-announce iff --member-id provided
1749
+ let selfAnnounceFn = null;
1750
+ if (options.memberId) {
1751
+ const { buildAnnounce } = loadFederationMemberForAnnounce(
1752
+ federationId,
1753
+ options.memberId,
1754
+ options.ttl,
1755
+ );
1756
+
1757
+ selfAnnounceFn = () => {
1758
+ const ann = buildAnnounce();
1759
+ const written = publishAnnounce(options.dropZone, ann);
1760
+ return { announce: ann, file: written };
1761
+ };
1762
+ }
1763
+
1764
+ function scanAndIngest() {
1765
+ const entries = scanDropZone(options.dropZone, federationId);
1766
+ let accepted = 0;
1767
+ let rejected = 0;
1768
+ const failures = [];
1769
+ for (const e of entries) {
1770
+ if (e.error) {
1771
+ rejected++;
1772
+ failures.push({ file: e.file, code: "PARSE_ERROR" });
1773
+ continue;
1774
+ }
1775
+ const r = cache.ingest(e.announce);
1776
+ if (r.accepted) accepted++;
1777
+ else {
1778
+ rejected++;
1779
+ failures.push({ file: e.file, code: r.reason });
1780
+ }
1781
+ }
1782
+ return { scanned: entries.length, accepted, rejected, failures };
1783
+ }
1784
+
1785
+ function snapshot() {
1786
+ return {
1787
+ federation_id: federationId,
1788
+ drop_zone: options.dropZone,
1789
+ members: cache.listMembers(federationId).map((m) => ({
1790
+ member_id: m.member_id,
1791
+ issuer: m.issuer,
1792
+ alg: m.alg,
1793
+ pubkey_id: m.pubkey_id,
1794
+ announced_at: m.announced_at,
1795
+ ttl_seconds: m.ttl_seconds,
1796
+ })),
1797
+ };
1798
+ }
1799
+
1800
+ // First pass: announce self + scan
1801
+ let selfFile = null;
1802
+ if (selfAnnounceFn) {
1803
+ const r = selfAnnounceFn();
1804
+ selfFile = r.file;
1805
+ }
1806
+ const firstScan = scanAndIngest();
1807
+
1808
+ if (options.once) {
1809
+ const out = {
1810
+ ok: true,
1811
+ federation_id: federationId,
1812
+ self_announce_file: selfFile,
1813
+ scan: firstScan,
1814
+ ...snapshot(),
1815
+ };
1816
+ if (options.json) {
1817
+ console.log(JSON.stringify(out, null, 2));
1818
+ } else {
1819
+ logger.success(
1820
+ `discovered ${out.members.length} member(s) in federation ${federationId}`,
1821
+ );
1822
+ for (const m of out.members) {
1823
+ logger.log(
1824
+ ` · ${chalk.green(m.member_id)} (${m.alg}) ${chalk.gray(m.pubkey_id.slice(0, 18) + "…")}`,
1825
+ );
1826
+ }
1827
+ }
1828
+ return;
1829
+ }
1830
+
1831
+ // Daemon: re-announce + re-scan on intervals
1832
+ const reannounceMs = Math.max(60, Math.floor(options.ttl / 3)) * 1000;
1833
+ const scanMs = Math.max(1, options.scanInterval) * 1000;
1834
+
1835
+ let scanTimer = null;
1836
+ let announceTimer = null;
1837
+ function cleanup() {
1838
+ if (scanTimer) clearInterval(scanTimer);
1839
+ if (announceTimer) clearInterval(announceTimer);
1840
+ process.exit(0);
1841
+ }
1842
+ process.once("SIGINT", cleanup);
1843
+ process.once("SIGTERM", cleanup);
1844
+
1845
+ // Re-entrancy guard: if a scan tick takes longer than scanInterval (large
1846
+ // drop-zone, slow disk), don't let setInterval stack up overlapping ticks.
1847
+ let scanInProgress = false;
1848
+ scanTimer = setInterval(() => {
1849
+ if (scanInProgress) return;
1850
+ scanInProgress = true;
1851
+ try {
1852
+ const r = scanAndIngest();
1853
+ if (options.json) {
1854
+ console.log(
1855
+ JSON.stringify({ tick: "scan", ...r, ...snapshot() }, null, 2),
1856
+ );
1857
+ } else {
1858
+ logger.log(
1859
+ `[${new Date().toISOString()}] scan: ${r.accepted}+/${r.scanned} accepted, ${cache.listMembers(federationId).length} live`,
1860
+ );
1861
+ }
1862
+ } catch (err) {
1863
+ logger.error(`scan failed: ${err.message}`);
1864
+ } finally {
1865
+ scanInProgress = false;
1866
+ }
1867
+ }, scanMs);
1868
+
1869
+ if (selfAnnounceFn) {
1870
+ announceTimer = setInterval(() => {
1871
+ try {
1872
+ selfAnnounceFn();
1873
+ if (!options.json) {
1874
+ logger.log(
1875
+ `[${new Date().toISOString()}] re-announced self in federation ${federationId}`,
1876
+ );
1877
+ }
1878
+ } catch (err) {
1879
+ logger.error(`self-announce failed: ${err.message}`);
1880
+ }
1881
+ }, reannounceMs);
1882
+ }
1883
+
1884
+ logger.success(
1885
+ `federation discover daemon running (drop-zone: ${options.dropZone}, scan: ${options.scanInterval}s, ttl: ${options.ttl}s)${
1886
+ selfAnnounceFn ? `, announcing as ${options.memberId}` : ", listen-only"
1887
+ }`,
1888
+ );
1889
+ await new Promise(() => {});
1890
+ }
1891
+
1892
+ const FEDERATION_TOPIC_PREFIX = "mtc-federation/v1";
1893
+
1894
+ function federationTopic(federationId) {
1895
+ return `${FEDERATION_TOPIC_PREFIX}/${federationId}`;
1896
+ }
1897
+
1898
+ async function runFederationDiscoverLibp2p(federationId, options) {
1899
+ const FederationAnnounceCache = mtcLib.FederationAnnounceCache;
1900
+ const { Libp2pTransport } =
1901
+ await import("@chainlesschain/core-mtc/transports/libp2p");
1902
+
1903
+ const cache = new FederationAnnounceCache({
1904
+ persistDir: options.cacheDir,
1905
+ });
1906
+
1907
+ let selfBuildAnnounce = null;
1908
+ let selfMember = null;
1909
+ if (options.memberId) {
1910
+ const { member, buildAnnounce } = loadFederationMemberForAnnounce(
1911
+ federationId,
1912
+ options.memberId,
1913
+ options.ttl,
1914
+ );
1915
+ selfBuildAnnounce = buildAnnounce;
1916
+ selfMember = member;
1917
+ }
1918
+
1919
+ // Spin up gossipsub libp2p node
1920
+ const node = await Libp2pTransport.create({
1921
+ listen: options.listen,
1922
+ mode: "gossipsub",
1923
+ });
1924
+
1925
+ // Helper: tear down the node on any error path so we don't leak the
1926
+ // libp2p host when initialization throws after node creation.
1927
+ const closeNodeOnError = async (err) => {
1928
+ try {
1929
+ await node.close();
1930
+ } catch (_e) {
1931
+ /* ignore close errors during error cleanup */
1932
+ }
1933
+ throw err;
1934
+ };
1935
+
1936
+ try {
1937
+ return await runFederationDiscoverLibp2pInner(
1938
+ federationId,
1939
+ options,
1940
+ node,
1941
+ cache,
1942
+ selfBuildAnnounce,
1943
+ selfMember,
1944
+ );
1945
+ } catch (err) {
1946
+ return closeNodeOnError(err);
1947
+ }
1948
+ }
1949
+
1950
+ async function runFederationDiscoverLibp2pInner(
1951
+ federationId,
1952
+ options,
1953
+ node,
1954
+ cache,
1955
+ selfBuildAnnounce,
1956
+ selfMember,
1957
+ ) {
1958
+ const topic = federationTopic(federationId);
1959
+
1960
+ // Subscribe + dispatch into cache
1961
+ let bytesReceived = 0;
1962
+ node.subscribeRaw(topic, (bytes) => {
1963
+ bytesReceived++;
1964
+ try {
1965
+ cache.ingest(JSON.parse(new TextDecoder().decode(bytes)));
1966
+ } catch (_err) {
1967
+ /* malformed announce — drop */
1968
+ }
1969
+ });
1970
+
1971
+ // Dial seed peers
1972
+ for (const peer of options.connect || []) {
1973
+ try {
1974
+ await node.connect(peer);
1975
+ } catch (err) {
1976
+ logger.warn(`connect to ${peer} failed: ${err.message}`);
1977
+ }
1978
+ }
1979
+
1980
+ // Mesh formation wait
1981
+ const meshWaitMs = Math.max(0, options.meshWaitMs ?? 1500);
1982
+ if (meshWaitMs > 0) await new Promise((r) => setTimeout(r, meshWaitMs));
1983
+
1984
+ async function publishSelf() {
1985
+ if (!selfBuildAnnounce) return null;
1986
+ const ann = selfBuildAnnounce();
1987
+ const result = await node.publishRaw(topic, JSON.stringify(ann));
1988
+ return { announce: ann, recipients: result.recipients };
1989
+ }
1990
+
1991
+ function snapshot() {
1992
+ return {
1993
+ federation_id: federationId,
1994
+ transport: "libp2p",
1995
+ multiaddrs: node.multiaddrs(),
1996
+ peer_id: node.peerIdString(),
1997
+ members: cache.listMembers(federationId).map((m) => ({
1998
+ member_id: m.member_id,
1999
+ issuer: m.issuer,
2000
+ alg: m.alg,
2001
+ pubkey_id: m.pubkey_id,
2002
+ announced_at: m.announced_at,
2003
+ ttl_seconds: m.ttl_seconds,
2004
+ })),
2005
+ };
2006
+ }
2007
+
2008
+ // First pass: announce self
2009
+ let firstPublish = null;
2010
+ if (selfBuildAnnounce) {
2011
+ firstPublish = await publishSelf();
2012
+ }
2013
+
2014
+ if (options.once) {
2015
+ // Wait briefly for any incoming announces from peers we just dialed
2016
+ if ((options.connect || []).length > 0) {
2017
+ await new Promise((r) => setTimeout(r, 1000));
2018
+ }
2019
+ const out = {
2020
+ ok: true,
2021
+ ...snapshot(),
2022
+ self_announce: firstPublish
2023
+ ? {
2024
+ member_id: options.memberId,
2025
+ issuer: selfMember?.issuer,
2026
+ recipients: firstPublish.recipients,
2027
+ }
2028
+ : null,
2029
+ bytes_received: bytesReceived,
2030
+ };
2031
+ if (options.json) {
2032
+ console.log(JSON.stringify(out, null, 2));
2033
+ } else {
2034
+ logger.success(
2035
+ `libp2p discover: peer_id=${out.peer_id}, ${out.members.length} member(s) cached`,
2036
+ );
2037
+ for (const a of out.multiaddrs) logger.log(` listen: ${a}`);
2038
+ for (const m of out.members) {
2039
+ logger.log(
2040
+ ` · ${chalk.green(m.member_id)} (${m.alg}) ${chalk.gray(m.pubkey_id.slice(0, 18) + "…")}`,
2041
+ );
2042
+ }
2043
+ }
2044
+ await node.close();
2045
+ return;
2046
+ }
2047
+
2048
+ // Daemon: re-announce on TTL/3
2049
+ const reannounceMs = Math.max(60, Math.floor(options.ttl / 3)) * 1000;
2050
+ let announceTimer = null;
2051
+ if (selfBuildAnnounce) {
2052
+ announceTimer = setInterval(async () => {
2053
+ try {
2054
+ await publishSelf();
2055
+ if (!options.json) {
2056
+ logger.log(
2057
+ `[${new Date().toISOString()}] re-announced via libp2p in federation ${federationId}`,
2058
+ );
2059
+ }
2060
+ } catch (err) {
2061
+ logger.error(`self-announce failed: ${err.message}`);
2062
+ }
2063
+ }, reannounceMs);
2064
+ }
2065
+
2066
+ const cleanup = async () => {
2067
+ if (announceTimer) clearInterval(announceTimer);
2068
+ try {
2069
+ await node.close();
2070
+ } catch (_err) {
2071
+ /* ignore */
2072
+ }
2073
+ process.exit(0);
2074
+ };
2075
+ process.once("SIGINT", cleanup);
2076
+ process.once("SIGTERM", cleanup);
2077
+
2078
+ logger.success(
2079
+ `federation discover daemon running (libp2p, peer_id: ${node.peerIdString()})${
2080
+ selfBuildAnnounce
2081
+ ? `, announcing as ${options.memberId} (TTL ${options.ttl}s)`
2082
+ : ", listen-only"
2083
+ }`,
2084
+ );
2085
+ for (const a of node.multiaddrs()) {
2086
+ logger.log(` listen: ${a}`);
2087
+ }
2088
+ await new Promise(() => {});
2089
+ }
2090
+
2091
+ // Internals exported for tests
2092
+ export const _federationInternals = {
2093
+ FEDERATION_REGISTRY_SCHEMA,
2094
+ loadFederationRegistry,
2095
+ saveFederationRegistry,
2096
+ getFederationDir,
2097
+ getFederationRegistryPath,
2098
+ publishAnnounce,
2099
+ scanDropZone,
2100
+ getDiscoverAnnouncesDir,
2101
+ };