chainlesschain 0.159.0 → 0.161.2

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-Cchx1iXI.css +1 -0
  4. package/src/assets/web-panel/assets/AIOps-CoZ9bIqF.js +1 -0
  5. package/src/assets/web-panel/assets/ActionButton-BvMi4awy.js +1 -0
  6. package/src/assets/web-panel/assets/Analytics-C1-TXmTC.css +1 -0
  7. package/src/assets/web-panel/assets/Analytics-hRk2ziup.js +3 -0
  8. package/src/assets/web-panel/assets/AppLayout-CtGprHSx.css +1 -0
  9. package/src/assets/web-panel/assets/AppLayout-_JR3Gko8.js +1 -0
  10. package/src/assets/web-panel/assets/Audit-6ZMsXmrO.css +1 -0
  11. package/src/assets/web-panel/assets/Audit-D8WmaHdX.js +1 -0
  12. package/src/assets/web-panel/assets/Backup-CY9QozR7.css +1 -0
  13. package/src/assets/web-panel/assets/Backup-CogYVeiE.js +1 -0
  14. package/src/assets/web-panel/assets/BaseInput-NAp5_OPY.js +1 -0
  15. package/src/assets/web-panel/assets/{Chat-DmX5bWvL.css → Chat-ByiYUboW.css} +1 -1
  16. package/src/assets/web-panel/assets/Chat-DkQnhjfk.js +2 -0
  17. package/src/assets/web-panel/assets/Checkbox-C9dkWb-7.js +1 -0
  18. package/src/assets/web-panel/assets/Codegen-BHyJ3j-p.js +1 -0
  19. package/src/assets/web-panel/assets/Codegen-BLP7id2a.css +1 -0
  20. package/src/assets/web-panel/assets/Col-JyQOivHb.js +1 -0
  21. package/src/assets/web-panel/assets/Community-C2RejeOY.css +1 -0
  22. package/src/assets/web-panel/assets/Community-UMq5QuBA.js +1 -0
  23. package/src/assets/web-panel/assets/Compact-DGlwooBJ.js +1 -0
  24. package/src/assets/web-panel/assets/Compliance-2rWGO55k.js +1 -0
  25. package/src/assets/web-panel/assets/Compliance-DOys4Ov1.css +1 -0
  26. package/src/assets/web-panel/assets/{Cowork-CFkkMMDK.js → Cowork-V-tDxtrt.js} +4 -4
  27. package/src/assets/web-panel/assets/Cron-YgEeQvdV.js +2 -0
  28. package/src/assets/web-panel/assets/Crosschain-C7Le4Pte.css +1 -0
  29. package/src/assets/web-panel/assets/Crosschain-Cgd5cRKn.js +1 -0
  30. package/src/assets/web-panel/assets/DID-BX6k3jZi.css +1 -0
  31. package/src/assets/web-panel/assets/DID-swdBCdMZ.js +2 -0
  32. package/src/assets/web-panel/assets/Dashboard-ClnWtxsT.js +3 -0
  33. package/src/assets/web-panel/assets/Dashboard-MFDcsVcM.css +1 -0
  34. package/src/assets/web-panel/assets/Dropdown-mlITwb7d.js +1 -0
  35. package/src/assets/web-panel/assets/Federation-CgmfLbx1.css +1 -0
  36. package/src/assets/web-panel/assets/Federation-DtUN3wQa.js +1 -0
  37. package/src/assets/web-panel/assets/{FormItemContext-Be6TSNxz.js → FormItemContext-BYmWDwAT.js} +1 -1
  38. package/src/assets/web-panel/assets/{Git-DGcuBXST.css → Git-DPuaGtg7.css} +1 -1
  39. package/src/assets/web-panel/assets/Git-Du1k1iHz.js +2 -0
  40. package/src/assets/web-panel/assets/Governance-B2TFaWsf.js +1 -0
  41. package/src/assets/web-panel/assets/Governance-BoipmXaM.css +1 -0
  42. package/src/assets/web-panel/assets/Inference-BWxYJF9-.css +1 -0
  43. package/src/assets/web-panel/assets/Inference-Cm_hmXla.js +1 -0
  44. package/src/assets/web-panel/assets/KnowledgeGraph-CztPDA96.css +1 -0
  45. package/src/assets/web-panel/assets/KnowledgeGraph-DLaLMo4r.js +1 -0
  46. package/src/assets/web-panel/assets/Logs-CElvIBBJ.js +2 -0
  47. package/src/assets/web-panel/assets/Marketplace-BlR3RCDV.js +1 -0
  48. package/src/assets/web-panel/assets/Marketplace-Djp5q9dS.css +1 -0
  49. package/src/assets/web-panel/assets/McpTools-BpHkrlka.js +5 -0
  50. package/src/assets/web-panel/assets/McpTools-CDjHmzxH.css +1 -0
  51. package/src/assets/web-panel/assets/Memory-Bcc2hxOA.css +1 -0
  52. package/src/assets/web-panel/assets/Memory-DeU9ys_m.js +2 -0
  53. package/src/assets/web-panel/assets/Mtc-CCJZpnJo.js +6 -0
  54. package/src/assets/web-panel/assets/Mtc-Cc8OJxe_.css +1 -0
  55. package/src/assets/web-panel/assets/NLProgramming-B_Tie6j1.js +1 -0
  56. package/src/assets/web-panel/assets/NLProgramming-CLOvy-35.css +1 -0
  57. package/src/assets/web-panel/assets/Notes-BcpuirPj.js +7 -0
  58. package/src/assets/web-panel/assets/Organization-BX-cIO8M.css +1 -0
  59. package/src/assets/web-panel/assets/Organization-B_SHESSc.js +4 -0
  60. package/src/assets/web-panel/assets/Overflow-R2SOGT0l.js +1 -0
  61. package/src/assets/web-panel/assets/{OverrideContext-GHsJf9ok.js → OverrideContext-Nubhv68k.js} +1 -1
  62. package/src/assets/web-panel/assets/P2P-Cx88UaiD.css +1 -0
  63. package/src/assets/web-panel/assets/P2P-IYYy3cEd.js +2 -0
  64. package/src/assets/web-panel/assets/Permissions-CR1N42yW.js +4 -0
  65. package/src/assets/web-panel/assets/Pipeline-DxkXqrH2.css +1 -0
  66. package/src/assets/web-panel/assets/Pipeline-nwFpKsU_.js +1 -0
  67. package/src/assets/web-panel/assets/Privacy-BGpz72PX.js +1 -0
  68. package/src/assets/web-panel/assets/Privacy-CrsfSFKd.css +1 -0
  69. package/src/assets/web-panel/assets/ProjectSettings-CEDhpgbs.js +2 -0
  70. package/src/assets/web-panel/assets/Projects-B5IgXt-x.css +1 -0
  71. package/src/assets/web-panel/assets/Projects-DABi6ylb.js +2 -0
  72. package/src/assets/web-panel/assets/Providers-HzrcE8ma.js +2 -0
  73. package/src/assets/web-panel/assets/QuickAsk-6FgX9DC6.js +1 -0
  74. package/src/assets/web-panel/assets/Recommend-BPhQwye7.js +1 -0
  75. package/src/assets/web-panel/assets/Recommend-CH6wKzGo.css +1 -0
  76. package/src/assets/web-panel/assets/Reputation-BL6hTN1s.js +1 -0
  77. package/src/assets/web-panel/assets/Reputation-D6VPNEd0.css +1 -0
  78. package/src/assets/web-panel/assets/Row-2akLU3YS.js +1 -0
  79. package/src/assets/web-panel/assets/RssFeed-D6qNq6Ht.js +3 -0
  80. package/src/assets/web-panel/assets/Search-B6RalzTB.css +1 -0
  81. package/src/assets/web-panel/assets/Search-R_b-u9oL.js +1 -0
  82. package/src/assets/web-panel/assets/Security-13K57V_v.css +1 -0
  83. package/src/assets/web-panel/assets/Security-DdW4hu_4.js +4 -0
  84. package/src/assets/web-panel/assets/Services-CnzEzGFN.js +2 -0
  85. package/src/assets/web-panel/assets/Skeleton-D6RevdW2.js +8 -0
  86. package/src/assets/web-panel/assets/Skills-DD5ReHH7.js +1 -0
  87. package/src/assets/web-panel/assets/Sla-CaQOOsjD.js +1 -0
  88. package/src/assets/web-panel/assets/Sla-K19oOyQc.css +1 -0
  89. package/src/assets/web-panel/assets/SpeechSettings-D-pGIn9Z.js +1 -0
  90. package/src/assets/web-panel/assets/SpeechSettings-DYPJTDKz.css +1 -0
  91. package/src/assets/web-panel/assets/Tasks-BbdO_i4Q.js +1 -0
  92. package/src/assets/web-panel/assets/Templates-BWTV8-2E.css +1 -0
  93. package/src/assets/web-panel/assets/Templates-CupAugDn.js +1 -0
  94. package/src/assets/web-panel/assets/Tenant-D3zkSAV0.css +1 -0
  95. package/src/assets/web-panel/assets/Tenant-rseAzHcY.js +1 -0
  96. package/src/assets/web-panel/assets/Tokens-BBOdNRHQ.css +1 -0
  97. package/src/assets/web-panel/assets/Tokens-DXMokNbR.js +1 -0
  98. package/src/assets/web-panel/assets/Trigger-DkSZjOlY.js +1 -0
  99. package/src/assets/web-panel/assets/Trust-CKb7QDH1.js +1 -0
  100. package/src/assets/web-panel/assets/Trust-DeOo0xAh.css +1 -0
  101. package/src/assets/web-panel/assets/UkeySign-Cux8_Ib_.js +1 -0
  102. package/src/assets/web-panel/assets/VideoEditing-CSOjdBZg.js +1 -0
  103. package/src/assets/web-panel/assets/VideoEditing-DksiizfS.css +1 -0
  104. package/src/assets/web-panel/assets/Wallet-DHbi5dHt.js +4 -0
  105. package/src/assets/web-panel/assets/Wallet-gR0ZvZbK.css +1 -0
  106. package/src/assets/web-panel/assets/WebAuthn-BkGDI33-.js +5 -0
  107. package/src/assets/web-panel/assets/WebAuthn-SSh5VhVO.css +1 -0
  108. package/src/assets/web-panel/assets/WorkflowEditor-BFZ3RYva.js +1 -0
  109. package/src/assets/web-panel/assets/WorkflowEditor-IiwsD8Kh.css +1 -0
  110. package/src/assets/web-panel/assets/{chat-DY27mJje.js → chat-BQ-Nk2XY.js} +1 -1
  111. package/src/assets/web-panel/assets/{collapseMotion-CyadT_6x.js → collapseMotion-BIjDVXtT.js} +1 -1
  112. package/src/assets/web-panel/assets/{colors-B7fDvuJc.js → colors-D2tTvuDI.js} +1 -1
  113. package/src/assets/web-panel/assets/{compact-item-DgthOVXi.js → compact-item-CqCEUZiy.js} +1 -1
  114. package/src/assets/web-panel/assets/{createContext-Blw2vgkG.js → createContext-C6HFlAQP.js} +1 -1
  115. package/src/assets/web-panel/assets/echarts-DmBLM6YO.js +19 -0
  116. package/src/assets/web-panel/assets/{hasIn-BfL1HJZl.js → hasIn-yp2CbhYc.js} +1 -1
  117. package/src/assets/web-panel/assets/icons-DvZE-RKs.js +57 -0
  118. package/src/assets/web-panel/assets/index-4cn1LmJ9.js +1 -0
  119. package/src/assets/web-panel/assets/index-B0jkl2Zb.js +1 -0
  120. package/src/assets/web-panel/assets/{index-BHnaIQEm.js → index-B2qFUwGb.js} +2 -2
  121. package/src/assets/web-panel/assets/index-B74gWYqD.js +1 -0
  122. package/src/assets/web-panel/assets/index-B8Qxu0q2.js +1 -0
  123. package/src/assets/web-panel/assets/index-B9b_mz4I.js +55 -0
  124. package/src/assets/web-panel/assets/index-BAA1SFp1.js +1 -0
  125. package/src/assets/web-panel/assets/index-BJeE7n_I.js +12 -0
  126. package/src/assets/web-panel/assets/index-BJoK7MkB.js +13 -0
  127. package/src/assets/web-panel/assets/{index-DyRzaN4b.js → index-BTL2yIvT.js} +3 -3
  128. package/src/assets/web-panel/assets/index-BlVnFOFL.js +1 -0
  129. package/src/assets/web-panel/assets/{index-x8iHfZRd.js → index-Blq49aTW.js} +3 -3
  130. package/src/assets/web-panel/assets/{index-BsleJWGy.js → index-BxSsO6Sm.js} +2 -2
  131. package/src/assets/web-panel/assets/index-CA3g3EpL.js +1 -0
  132. package/src/assets/web-panel/assets/{index-BuEOhTAw.js → index-CFoFkVUt.js} +5 -5
  133. package/src/assets/web-panel/assets/index-CWhXxdyo.js +1 -0
  134. package/src/assets/web-panel/assets/index-CXf0zL5i.js +13 -0
  135. package/src/assets/web-panel/assets/index-CcwZodUl.js +1 -0
  136. package/src/assets/web-panel/assets/index-Cd6m6ynF.js +1 -0
  137. package/src/assets/web-panel/assets/index-CfX1DEtk.css +1 -0
  138. package/src/assets/web-panel/assets/index-Cfy9l115.js +1 -0
  139. package/src/assets/web-panel/assets/index-CijiVpfO.js +1 -0
  140. package/src/assets/web-panel/assets/index-Cpfx7-LN.js +12 -0
  141. package/src/assets/web-panel/assets/index-CrEEL63u.js +1 -0
  142. package/src/assets/web-panel/assets/index-CucxAdwN.js +6 -0
  143. package/src/assets/web-panel/assets/index-CvuBD5TK.js +3 -0
  144. package/src/assets/web-panel/assets/index-Cw4v7ezB.js +65 -0
  145. package/src/assets/web-panel/assets/{index-00RNC3ZQ.js → index-D34iabcS.js} +8 -8
  146. package/src/assets/web-panel/assets/index-DEKuiAPQ.js +3 -0
  147. package/src/assets/web-panel/assets/index-Dj4P0iWm.js +1 -0
  148. package/src/assets/web-panel/assets/index-DjeNNVwu.js +1 -0
  149. package/src/assets/web-panel/assets/index-DlgMVieO.js +1 -0
  150. package/src/assets/web-panel/assets/{index-CiN4NEAa.js → index-DnehXcB-.js} +2 -2
  151. package/src/assets/web-panel/assets/index-JszcDpsT.js +7 -0
  152. package/src/assets/web-panel/assets/index-KdFFI-p3.js +1 -0
  153. package/src/assets/web-panel/assets/index-NNymVAza.js +3 -0
  154. package/src/assets/web-panel/assets/index-aw2DwKj-.js +21 -0
  155. package/src/assets/web-panel/assets/index-nzDx0JAR.js +1 -0
  156. package/src/assets/web-panel/assets/index-w1xShUDf.js +1 -0
  157. package/src/assets/web-panel/assets/{initDefaultProps-BeWIEzBr.js → initDefaultProps-DfdVxwz6.js} +1 -1
  158. package/src/assets/web-panel/assets/motion-CL0bdvJg.js +11 -0
  159. package/src/assets/web-panel/assets/move-Bo9Fgzv7.js +4 -0
  160. package/src/assets/web-panel/assets/mtc-parser-pGMSt10g.js +1 -0
  161. package/src/assets/web-panel/assets/{omit-BS0H_YEP.js → omit-C-cc6wHr.js} +1 -1
  162. package/src/assets/web-panel/assets/{pickAttrs-BOGgGau8.js → pickAttrs-CjLp5RN-.js} +1 -1
  163. package/src/assets/web-panel/assets/{placementArrow--c5TQkDQ.js → placementArrow-CJa8gsqa.js} +1 -1
  164. package/src/assets/web-panel/assets/{responsiveObserve-CrHOX7jp.js → responsiveObserve-CcDj3P-p.js} +1 -1
  165. package/src/assets/web-panel/assets/slide-CpvbHO26.js +4 -0
  166. package/src/assets/web-panel/assets/statusUtils-BK69kP1U.js +1 -0
  167. package/src/assets/web-panel/assets/{styleChecker-CiDrXnbi.js → styleChecker-CDvBRzsG.js} +1 -1
  168. package/src/assets/web-panel/assets/useFlexGapSupport-CRN_hzJt.js +1 -0
  169. package/src/assets/web-panel/assets/useFs-BD-YRwbU.js +1 -0
  170. package/src/assets/web-panel/assets/{useMergedState-CLY-UHry.js → useMergedState-TP9VIF2K.js} +1 -1
  171. package/src/assets/web-panel/assets/{useRefs-HzOoMkZk.js → useRefs-BhIz_lC3.js} +1 -1
  172. package/src/assets/web-panel/assets/{useState-yWVBuz1S.js → useState-CpKsyozn.js} +1 -1
  173. package/src/assets/web-panel/assets/vendor-B6ToihkA.js +1 -0
  174. package/src/assets/web-panel/assets/vnode-Bzp-FsbB.js +1 -0
  175. package/src/assets/web-panel/assets/{ws-4ur1fGsk.js → ws-D_5-FRIb.js} +1 -1
  176. package/src/assets/web-panel/assets/zoom-BHqpWXJV.js +4 -0
  177. package/src/assets/web-panel/index.html +4 -4
  178. package/src/commands/audit.js +296 -0
  179. package/src/commands/crosschain.js +564 -8
  180. package/src/commands/init.js +16 -1
  181. package/src/commands/mtc.js +2735 -86
  182. package/src/lib/audit-mtc.js +504 -0
  183. package/src/lib/cross-chain-mtc.js +904 -0
  184. package/src/assets/web-panel/assets/AIOps-Bfzpnnlg.css +0 -1
  185. package/src/assets/web-panel/assets/AIOps-ztGJppiG.js +0 -1
  186. package/src/assets/web-panel/assets/ActionButton-DCuat-R-.js +0 -1
  187. package/src/assets/web-panel/assets/Analytics-BFI7jbwM.css +0 -1
  188. package/src/assets/web-panel/assets/Analytics-m_EtU-RR.js +0 -3
  189. package/src/assets/web-panel/assets/AppLayout-8HizVjee.css +0 -1
  190. package/src/assets/web-panel/assets/AppLayout-Crz8jbEC.js +0 -1
  191. package/src/assets/web-panel/assets/Audit-CNLWjMXN.js +0 -1
  192. package/src/assets/web-panel/assets/Audit-kU7vbN-D.css +0 -1
  193. package/src/assets/web-panel/assets/Backup-CggaSq9O.js +0 -1
  194. package/src/assets/web-panel/assets/Backup-DxLiJzmk.css +0 -1
  195. package/src/assets/web-panel/assets/BaseInput-BssiYvl6.js +0 -1
  196. package/src/assets/web-panel/assets/Chat-tuVfpQUO.js +0 -2
  197. package/src/assets/web-panel/assets/Checkbox-BUUkXdQg.js +0 -1
  198. package/src/assets/web-panel/assets/Codegen-AVAcL7NA.css +0 -1
  199. package/src/assets/web-panel/assets/Codegen-CHu-7YGr.js +0 -1
  200. package/src/assets/web-panel/assets/Col-2IaMFwmX.js +0 -1
  201. package/src/assets/web-panel/assets/Community-CYL7Fvjq.js +0 -1
  202. package/src/assets/web-panel/assets/Community-DqDfLQui.css +0 -1
  203. package/src/assets/web-panel/assets/Compact-5yRVELhA.js +0 -1
  204. package/src/assets/web-panel/assets/Compliance-CKuIDJHK.js +0 -1
  205. package/src/assets/web-panel/assets/Compliance-CKxw6vIq.css +0 -1
  206. package/src/assets/web-panel/assets/Cron-DIfkI7vf.js +0 -2
  207. package/src/assets/web-panel/assets/Crosschain-BkjY-lst.js +0 -1
  208. package/src/assets/web-panel/assets/Crosschain-DThGgQk8.css +0 -1
  209. package/src/assets/web-panel/assets/DID-BDvsVa08.css +0 -1
  210. package/src/assets/web-panel/assets/DID-ChzRkgNy.js +0 -2
  211. package/src/assets/web-panel/assets/Dashboard-Cviwdc26.css +0 -1
  212. package/src/assets/web-panel/assets/Dashboard-DKrbXVNn.js +0 -3
  213. package/src/assets/web-panel/assets/Dropdown-BYGoxH1z.js +0 -1
  214. package/src/assets/web-panel/assets/Federation-B8QX-IaA.js +0 -1
  215. package/src/assets/web-panel/assets/Federation-BftELHDw.css +0 -1
  216. package/src/assets/web-panel/assets/Git-BBnGgbBR.js +0 -2
  217. package/src/assets/web-panel/assets/Governance-BfmfQBGB.css +0 -1
  218. package/src/assets/web-panel/assets/Governance-CxUHZMsp.js +0 -1
  219. package/src/assets/web-panel/assets/Inference-BlnOG71q.js +0 -1
  220. package/src/assets/web-panel/assets/Inference-EFFc7eNZ.css +0 -1
  221. package/src/assets/web-panel/assets/Keyframes-C7fCrnlS.js +0 -1
  222. package/src/assets/web-panel/assets/KnowledgeGraph-U8ps3aGJ.css +0 -1
  223. package/src/assets/web-panel/assets/KnowledgeGraph-vkVq38kC.js +0 -19
  224. package/src/assets/web-panel/assets/Logs-Bxx7WARH.js +0 -2
  225. package/src/assets/web-panel/assets/Marketplace-B-4uYu_j.css +0 -1
  226. package/src/assets/web-panel/assets/Marketplace-Bh8ExT9_.js +0 -1
  227. package/src/assets/web-panel/assets/McpTools-CTQrNVAQ.css +0 -1
  228. package/src/assets/web-panel/assets/McpTools-Djj_a3ko.js +0 -5
  229. package/src/assets/web-panel/assets/Memory-DRghrGJr.css +0 -1
  230. package/src/assets/web-panel/assets/Memory-DlgNgAov.js +0 -2
  231. package/src/assets/web-panel/assets/NLProgramming-BN3jaoen.js +0 -1
  232. package/src/assets/web-panel/assets/NLProgramming-jURs-f-a.css +0 -1
  233. package/src/assets/web-panel/assets/Notes-De7mIkkV.js +0 -7
  234. package/src/assets/web-panel/assets/Organization-B-98mdK2.js +0 -4
  235. package/src/assets/web-panel/assets/Organization-DdOOM4ic.css +0 -1
  236. package/src/assets/web-panel/assets/Overflow-G0I8IlY3.js +0 -1
  237. package/src/assets/web-panel/assets/P2P-DIUgHZ1z.js +0 -2
  238. package/src/assets/web-panel/assets/P2P-OEzOeMZX.css +0 -1
  239. package/src/assets/web-panel/assets/Permissions-B8XycCVk.js +0 -4
  240. package/src/assets/web-panel/assets/Pipeline-DyqCLFVr.css +0 -1
  241. package/src/assets/web-panel/assets/Pipeline-cUpETlXS.js +0 -1
  242. package/src/assets/web-panel/assets/Portal-SsPhn64D.js +0 -1
  243. package/src/assets/web-panel/assets/Privacy-B6J89UBw.js +0 -1
  244. package/src/assets/web-panel/assets/Privacy-B_cAicd1.css +0 -1
  245. package/src/assets/web-panel/assets/ProjectSettings-2Ftw0zt_.js +0 -2
  246. package/src/assets/web-panel/assets/Projects-CcOdFpgr.js +0 -2
  247. package/src/assets/web-panel/assets/Projects-DxKelI5h.css +0 -1
  248. package/src/assets/web-panel/assets/Providers-D0r2qSf-.js +0 -2
  249. package/src/assets/web-panel/assets/QuickAsk-CwsPpfkq.js +0 -1
  250. package/src/assets/web-panel/assets/Recommend-BYEDetJm.js +0 -1
  251. package/src/assets/web-panel/assets/Recommend-DgNSCgRX.css +0 -1
  252. package/src/assets/web-panel/assets/Reputation-Bli4hBGH.js +0 -1
  253. package/src/assets/web-panel/assets/Reputation-y-46ThW8.css +0 -1
  254. package/src/assets/web-panel/assets/Row-DTW9_BYi.js +0 -1
  255. package/src/assets/web-panel/assets/RssFeed-kwj_himl.js +0 -3
  256. package/src/assets/web-panel/assets/Search-BTk9rglb.css +0 -1
  257. package/src/assets/web-panel/assets/Search-DymcqASO.js +0 -1
  258. package/src/assets/web-panel/assets/Security-Bh3yvyNN.js +0 -4
  259. package/src/assets/web-panel/assets/Security-Dwxw7rfP.css +0 -1
  260. package/src/assets/web-panel/assets/Services-O4UZaYur.js +0 -2
  261. package/src/assets/web-panel/assets/Skeleton-B82oQZTz.js +0 -8
  262. package/src/assets/web-panel/assets/Skills-BfkrC05g.js +0 -1
  263. package/src/assets/web-panel/assets/Sla-C1WYuQKf.css +0 -1
  264. package/src/assets/web-panel/assets/Sla-DEs5XCIf.js +0 -1
  265. package/src/assets/web-panel/assets/SpeechSettings-BWMhb10j.js +0 -1
  266. package/src/assets/web-panel/assets/SpeechSettings-CiKvsIyV.css +0 -1
  267. package/src/assets/web-panel/assets/Tasks-CIBUb9M1.js +0 -1
  268. package/src/assets/web-panel/assets/Templates-DAkzr0xS.css +0 -1
  269. package/src/assets/web-panel/assets/Templates-iqSuJY-O.js +0 -1
  270. package/src/assets/web-panel/assets/Tenant-BJr-h-_0.css +0 -1
  271. package/src/assets/web-panel/assets/Tenant-CLqZjkVq.js +0 -1
  272. package/src/assets/web-panel/assets/Tokens-Bwz8aQtK.js +0 -1
  273. package/src/assets/web-panel/assets/Tokens-KvJRHQcl.css +0 -1
  274. package/src/assets/web-panel/assets/Trigger-CxnChVoS.js +0 -1
  275. package/src/assets/web-panel/assets/Trust-BLI308Ik.css +0 -1
  276. package/src/assets/web-panel/assets/Trust-Bp23lyZl.js +0 -1
  277. package/src/assets/web-panel/assets/UkeySign-_VAMquoh.js +0 -1
  278. package/src/assets/web-panel/assets/VideoEditing-BA1N-5kq.css +0 -1
  279. package/src/assets/web-panel/assets/VideoEditing-Bxqwg4zW.js +0 -1
  280. package/src/assets/web-panel/assets/Wallet-Cvht6Yrh.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-DD4EomJu.js +0 -5
  284. package/src/assets/web-panel/assets/WorkflowEditor-CEKNTS5G.css +0 -1
  285. package/src/assets/web-panel/assets/WorkflowEditor-CJy5e0fl.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-CJACPYXu.js +0 -57
  288. package/src/assets/web-panel/assets/index-B9B9Zbf3.js +0 -1
  289. package/src/assets/web-panel/assets/index-BAsMNMbh.js +0 -1
  290. package/src/assets/web-panel/assets/index-BB5y5Y0z.js +0 -14
  291. package/src/assets/web-panel/assets/index-BVjDIZQ4.js +0 -1
  292. package/src/assets/web-panel/assets/index-BZeRnzuY.js +0 -55
  293. package/src/assets/web-panel/assets/index-BZfoAtZC.js +0 -1
  294. package/src/assets/web-panel/assets/index-Bl2nXm2J.js +0 -13
  295. package/src/assets/web-panel/assets/index-Bw1dwHWm.js +0 -1
  296. package/src/assets/web-panel/assets/index-Bw_UV2ez.js +0 -13
  297. package/src/assets/web-panel/assets/index-C8GYpC65.js +0 -1
  298. package/src/assets/web-panel/assets/index-CBHlKa-J.js +0 -3
  299. package/src/assets/web-panel/assets/index-CL-7KCFI.js +0 -3
  300. package/src/assets/web-panel/assets/index-CqjUUbil.js +0 -12
  301. package/src/assets/web-panel/assets/index-Cqtt1N0F.js +0 -1
  302. package/src/assets/web-panel/assets/index-CyGyEIVX.css +0 -1
  303. package/src/assets/web-panel/assets/index-D1QP9Ue1.js +0 -1
  304. package/src/assets/web-panel/assets/index-D6BUjL6I.js +0 -1
  305. package/src/assets/web-panel/assets/index-DN7ywgBJ.js +0 -1
  306. package/src/assets/web-panel/assets/index-DasW8LIs.js +0 -1
  307. package/src/assets/web-panel/assets/index-Dd9McFt_.js +0 -1
  308. package/src/assets/web-panel/assets/index-DdzLEdL6.js +0 -7
  309. package/src/assets/web-panel/assets/index-DhML6F3z.js +0 -1
  310. package/src/assets/web-panel/assets/index-DjN0nHiV.js +0 -1
  311. package/src/assets/web-panel/assets/index-DjhWpiZW.js +0 -1
  312. package/src/assets/web-panel/assets/index-DouFuKIR.js +0 -1
  313. package/src/assets/web-panel/assets/index-DzWwjWYN.js +0 -21
  314. package/src/assets/web-panel/assets/index-Dza_w3kG.js +0 -1
  315. package/src/assets/web-panel/assets/index-Q4qgOtOe.js +0 -12
  316. package/src/assets/web-panel/assets/index-U4Zd5IK6.js +0 -8
  317. package/src/assets/web-panel/assets/index-Vit9TJBu.js +0 -36
  318. package/src/assets/web-panel/assets/index-lkJllVbJ.js +0 -6
  319. package/src/assets/web-panel/assets/index-uMWaXVtR.js +0 -3
  320. package/src/assets/web-panel/assets/index-uwh_ikIt.js +0 -1
  321. package/src/assets/web-panel/assets/motion-CIQOKmi6.js +0 -11
  322. package/src/assets/web-panel/assets/move-ZgRPlBji.js +0 -4
  323. package/src/assets/web-panel/assets/slide-Dd2mJUD0.js +0 -4
  324. package/src/assets/web-panel/assets/statusUtils-5QFvAofV.js +0 -1
  325. package/src/assets/web-panel/assets/transition-CVd2ueaJ.js +0 -1
  326. package/src/assets/web-panel/assets/useConfigInject-DIkkDpt1.js +0 -2
  327. package/src/assets/web-panel/assets/useFlexGapSupport-rrtj6f1h.js +0 -1
  328. package/src/assets/web-panel/assets/useFs-CK8VYPus.js +0 -1
  329. package/src/assets/web-panel/assets/vendor-D0Qjn73K.js +0 -1
  330. package/src/assets/web-panel/assets/vnode-xRp-KMjS.js +0 -1
  331. package/src/assets/web-panel/assets/zoom-CrI_kdTW.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
+ }
127
+
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
+ }
85
143
 
86
- const keys = loadOrGenerateKeyPair(opts.secretKeyFile);
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
+ }
87
171
 
88
- const leafHashes = rawLeaves.map((l) => leafHash(jcs(l)));
89
- const tree = new MerkleTree(leafHashes);
90
- const root = tree.root();
172
+ const threshold = Number.isInteger(opts.threshold)
173
+ ? opts.threshold
174
+ : members.length;
91
175
 
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,
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,2147 @@ 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");
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);
786
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
+ registerFederationGovernanceCommands(fed);
1645
+ }
1646
+
1647
+ // ─────────────────────────────────────────────────────────────────────────
1648
+ // Federation governance log (MTC_联邦治理_v1.md §9.1) — 8 subcommands
1649
+ // ─────────────────────────────────────────────────────────────────────────
1650
+
1651
+ function getGovernanceLogPath(federationId) {
1652
+ return path.join(getFederationDir(), "governance", `${federationId}.jsonl`);
1653
+ }
1654
+
1655
+ function loadGovernanceLog(federationId) {
1656
+ const file = getGovernanceLogPath(federationId);
1657
+ if (!fs.existsSync(file)) return [];
1658
+ const raw = fs.readFileSync(file, "utf-8");
1659
+ const events = [];
1660
+ for (const line of raw.split(/\r?\n/)) {
1661
+ if (!line.trim()) continue;
1662
+ try {
1663
+ events.push(JSON.parse(line));
1664
+ } catch (_err) {
1665
+ /* skip corrupt line — replay-from-source-of-truth, don't crash */
1666
+ }
1667
+ }
1668
+ return events;
1669
+ }
1670
+
1671
+ function appendGovernanceEvent(federationId, event) {
1672
+ const file = getGovernanceLogPath(federationId);
1673
+ fs.mkdirSync(path.dirname(file), { recursive: true });
1674
+ fs.appendFileSync(file, JSON.stringify(event) + "\n", "utf-8");
1675
+ }
1676
+
1677
+ /**
1678
+ * Look up a member's keys + alg for signing a governance event as that
1679
+ * member. Throws if not joined.
1680
+ */
1681
+ function loadMemberSigner(federationId, memberId) {
1682
+ const registry = loadFederationRegistry();
1683
+ const fedEntry = registry.federations[federationId];
1684
+ if (!fedEntry || !fedEntry.members[memberId]) {
1685
+ throw new Error(
1686
+ `not joined as "${memberId}" in federation "${federationId}" — \`cc mtc federation join ${federationId} --member-id ${memberId}\` first`,
1687
+ );
1688
+ }
1689
+ const member = fedEntry.members[memberId];
1690
+ if (!member.key_file || !fs.existsSync(member.key_file)) {
1691
+ throw new Error(`member key file missing: ${member.key_file}`);
1692
+ }
1693
+ let signerInfo;
1694
+ if (member.alg === "Ed25519") signerInfo = resolveSigner("ed25519");
1695
+ else if (member.alg === "SLH-DSA-SHA2-128F")
1696
+ signerInfo = resolveSigner("slh-dsa-128f");
1697
+ else throw new Error(`unknown member alg: ${member.alg}`);
1698
+ const keys = loadOrGenerateKeyPair(member.key_file, signerInfo);
1699
+ return { member, keys, alg: member.alg };
1700
+ }
1701
+
1702
+ function emitAndPersist(federationId, params) {
1703
+ const event = mtcLib.createGovernanceEvent({
1704
+ federationId,
1705
+ ...params,
1706
+ });
1707
+ appendGovernanceEvent(federationId, event);
1708
+ return event;
1709
+ }
1710
+
1711
+ /**
1712
+ * Pre-flight check that a confirm-* event has its matching propose-*.
1713
+ * Reads the local governance log + checks for an unresolved proposal.
1714
+ * Throws on missing — caller decides whether to log a warning or hard-fail.
1715
+ *
1716
+ * @param {string} federationId
1717
+ * @param {string} proposalType — "propose-revoke" | "propose-threshold"
1718
+ * @param {(payload: object) => boolean} matcher — returns true when payload matches
1719
+ */
1720
+ function requireOpenProposal(federationId, proposalType, matcher) {
1721
+ const events = loadGovernanceLog(federationId);
1722
+ // Walk forward; an open proposal is one not yet matched by a confirm of
1723
+ // the same target. v0.1 quorum gating only checks "is there at least one
1724
+ // proposal event with a matching target" — full quorum cooldown logic
1725
+ // lives in lib replay, this is a CLI-side guardrail.
1726
+ // Matcher signature: (payload, event) => boolean
1727
+ const open = events.some(
1728
+ (e) =>
1729
+ e && e.event_type === proposalType && e.payload && matcher(e.payload, e),
1730
+ );
1731
+ if (!open) {
1732
+ throw new Error(
1733
+ `no open ${proposalType} proposal matches — emit ${proposalType} first`,
1734
+ );
1735
+ }
1736
+ }
1737
+
1738
+ /**
1739
+ * Internal helper used by both the `governance-publish` CLI handler and
1740
+ * the `governance-sync-serve` daemon. Pure side-effect free aside from
1741
+ * filesystem writes to <drop-zone>/federation-governance/<fed>/.
1742
+ *
1743
+ * @returns {{ federation_id, drop_zone, local_total, published, skipped }}
1744
+ */
1745
+ function runGovernancePublish(federationId, dropZone) {
1746
+ const events = loadGovernanceLog(federationId);
1747
+ const targetDir = path.join(dropZone, "federation-governance", federationId);
1748
+ fs.mkdirSync(targetDir, { recursive: true });
1749
+ let published = 0;
1750
+ let skipped = 0;
1751
+ for (const ev of events) {
1752
+ if (!ev || typeof ev.event_id !== "string") continue;
1753
+ const target = path.join(targetDir, `${ev.event_id}.json`);
1754
+ if (fs.existsSync(target)) {
1755
+ skipped++;
1756
+ continue;
1757
+ }
1758
+ const tmp = `${target}.${process.pid}.tmp`;
1759
+ fs.writeFileSync(tmp, JSON.stringify(ev, null, 2), "utf-8");
1760
+ fs.renameSync(tmp, target);
1761
+ published++;
1762
+ }
1763
+ return {
1764
+ federation_id: federationId,
1765
+ drop_zone: targetDir,
1766
+ local_total: events.length,
1767
+ published,
1768
+ skipped,
1769
+ };
1770
+ }
1771
+
1772
+ /**
1773
+ * Internal helper for governance-pull + governance-sync-serve.
1774
+ * Reads remote events from drop-zone, optionally signature-verifies them,
1775
+ * dedupes by event_id against local log, sorts chronologically, and appends
1776
+ * new events to the local jsonl.
1777
+ *
1778
+ * @returns {{
1779
+ * federation_id, drop_zone, remote_total, local_total_before,
1780
+ * appended, duplicates, invalid_signature, unknown_signer
1781
+ * }}
1782
+ */
1783
+ function statsPath(federationId) {
1784
+ return path.join(
1785
+ getFederationDir(),
1786
+ "governance",
1787
+ `${federationId}.sync-stats.json`,
1788
+ );
1789
+ }
1790
+
1791
+ function loadStats(federationId) {
1792
+ const file = statsPath(federationId);
1793
+ if (!fs.existsSync(file)) return {};
1794
+ try {
1795
+ return JSON.parse(fs.readFileSync(file, "utf-8"));
1796
+ } catch (_err) {
1797
+ return {};
1798
+ }
1799
+ }
1800
+
1801
+ function saveStats(federationId, stats) {
1802
+ const file = statsPath(federationId);
1803
+ fs.mkdirSync(path.dirname(file), { recursive: true });
1804
+ // Atomic write so polling readers never see partial JSON
1805
+ const tmp = `${file}.${process.pid}.tmp`;
1806
+ fs.writeFileSync(tmp, JSON.stringify(stats, null, 2), "utf-8");
1807
+ fs.renameSync(tmp, file);
1808
+ }
1809
+
1810
+ function runGovernancePull(federationId, dropZone, opts = {}) {
1811
+ const sourceDir = path.join(dropZone, "federation-governance", federationId);
1812
+ if (!fs.existsSync(sourceDir)) {
1813
+ // Daemon-friendly: empty drop-zone is "nothing to pull yet", not an error.
1814
+ return {
1815
+ federation_id: federationId,
1816
+ drop_zone: sourceDir,
1817
+ remote_total: 0,
1818
+ local_total_before: loadGovernanceLog(federationId).length,
1819
+ appended: 0,
1820
+ duplicates: 0,
1821
+ invalid_signature: 0,
1822
+ unknown_signer: 0,
1823
+ };
1824
+ }
1825
+
1826
+ const remote = [];
1827
+ for (const name of fs.readdirSync(sourceDir)) {
1828
+ if (!name.endsWith(".json")) continue;
1829
+ try {
1830
+ const ev = JSON.parse(
1831
+ fs.readFileSync(path.join(sourceDir, name), "utf-8"),
1832
+ );
1833
+ if (ev && typeof ev.event_id === "string") remote.push(ev);
1834
+ } catch (_err) {
1835
+ /* skip malformed */
1836
+ }
1837
+ }
1838
+
1839
+ let invalid = 0;
1840
+ let unknown = 0;
1841
+ let candidates = remote;
1842
+ if (opts.verify) {
1843
+ const registry = loadFederationRegistry();
1844
+ const fedEntry = registry.federations[federationId] || { members: {} };
1845
+ const lookup = (actor /* , keyId */) => {
1846
+ const m = fedEntry.members[actor];
1847
+ if (!m || !m.pubkey_jwk) return null;
1848
+ try {
1849
+ return Buffer.from(m.pubkey_jwk.x, "base64url");
1850
+ } catch (_err) {
1851
+ return null;
1852
+ }
1853
+ };
1854
+ const result = mtcLib.verifyGovernanceLog(remote, lookup);
1855
+ invalid = result.invalid.length;
1856
+ unknown = result.unknown.length;
1857
+ candidates = result.valid;
1858
+ }
1859
+
1860
+ const localEvents = loadGovernanceLog(federationId);
1861
+ const localIds = new Set(
1862
+ localEvents.filter((e) => e && e.event_id).map((e) => e.event_id),
1863
+ );
1864
+ const newEvents = candidates.filter((e) => !localIds.has(e.event_id));
1865
+ const sorted = mtcLib.sortGovernanceEventsChronologically(newEvents);
1866
+ for (const ev of sorted) appendGovernanceEvent(federationId, ev);
1867
+
1868
+ return {
1869
+ federation_id: federationId,
1870
+ drop_zone: sourceDir,
1871
+ remote_total: remote.length,
1872
+ local_total_before: localEvents.length,
1873
+ appended: sorted.length,
1874
+ duplicates: candidates.length - newEvents.length,
1875
+ invalid_signature: invalid,
1876
+ unknown_signer: unknown,
1877
+ };
1878
+ }
1879
+
1880
+ function registerFederationGovernanceCommands(fed) {
1881
+ // mtc federation invite — propose adding a candidate member
1882
+ fed
1883
+ .command("invite <federation-id> <candidate-member-id>")
1884
+ .description("Propose adding a candidate member (governance.log event)")
1885
+ .requiredOption("--actor <member-id>", "Existing member casting the invite")
1886
+ .requiredOption("--candidate-pubkey-id <id>", "Candidate's pubkey_id")
1887
+ .option("--candidate-alg <alg>", "ed25519 | slh-dsa-128f", "ed25519")
1888
+ .option("--json", "JSON output")
1889
+ .action((federationId, candidateMemberId, options) => {
1890
+ try {
1891
+ const { keys, alg } = loadMemberSigner(federationId, options.actor);
1892
+ const event = emitAndPersist(federationId, {
1893
+ eventType: "invite",
1894
+ actorMemberId: options.actor,
1895
+ secretKey: keys.secretKey,
1896
+ publicKey: keys.publicKey,
1897
+ alg,
1898
+ payload: {
1899
+ candidate_member_id: candidateMemberId,
1900
+ candidate_pubkey_id: options.candidatePubkeyId,
1901
+ candidate_alg: options.candidateAlg,
1902
+ },
1903
+ });
1904
+ if (options.json) return console.log(JSON.stringify(event, null, 2));
1905
+ logger.success(
1906
+ `Invited ${candidateMemberId} into ${federationId} (event ${event.event_id})`,
1907
+ );
1908
+ } catch (err) {
1909
+ logger.error(`mtc federation invite failed: ${err.message}`);
1910
+ process.exit(1);
1911
+ }
1912
+ });
1913
+
1914
+ // mtc federation vote — vote on an outstanding invite
1915
+ fed
1916
+ .command("vote <federation-id> <candidate-member-id>")
1917
+ .description("Vote on an outstanding invite")
1918
+ .requiredOption("--actor <member-id>", "Voting member")
1919
+ .requiredOption("--decision <approve|reject>", "Vote decision")
1920
+ .option("--json", "JSON output")
1921
+ .action((federationId, candidateMemberId, options) => {
1922
+ try {
1923
+ if (!["approve", "reject"].includes(options.decision)) {
1924
+ throw new Error("--decision must be approve or reject");
1925
+ }
1926
+ const { keys, alg } = loadMemberSigner(federationId, options.actor);
1927
+ const event = emitAndPersist(federationId, {
1928
+ eventType: "vote",
1929
+ actorMemberId: options.actor,
1930
+ secretKey: keys.secretKey,
1931
+ publicKey: keys.publicKey,
1932
+ alg,
1933
+ payload: {
1934
+ invite_target_member_id: candidateMemberId,
1935
+ decision: options.decision,
1936
+ },
1937
+ });
1938
+ if (options.json) return console.log(JSON.stringify(event, null, 2));
1939
+ logger.success(
1940
+ `${options.actor} voted ${options.decision} on ${candidateMemberId} (event ${event.event_id})`,
1941
+ );
1942
+ } catch (err) {
1943
+ logger.error(`mtc federation vote failed: ${err.message}`);
1944
+ process.exit(1);
1945
+ }
1946
+ });
1947
+
1948
+ // mtc federation propose-revoke
1949
+ fed
1950
+ .command("propose-revoke <federation-id> <target-member-id>")
1951
+ .description("Propose revoking a member (7-day grace period)")
1952
+ .requiredOption("--actor <member-id>", "Proposing member")
1953
+ .requiredOption("--reason <text>", "Reason (e.g. inactive, key-compromise)")
1954
+ .option("--json", "JSON output")
1955
+ .action((federationId, targetMemberId, options) => {
1956
+ try {
1957
+ const { keys, alg } = loadMemberSigner(federationId, options.actor);
1958
+ const event = emitAndPersist(federationId, {
1959
+ eventType: "propose-revoke",
1960
+ actorMemberId: options.actor,
1961
+ secretKey: keys.secretKey,
1962
+ publicKey: keys.publicKey,
1963
+ alg,
1964
+ payload: { target_member_id: targetMemberId, reason: options.reason },
1965
+ });
1966
+ if (options.json) return console.log(JSON.stringify(event, null, 2));
1967
+ logger.success(
1968
+ `Proposed revoke of ${targetMemberId} (reason: ${options.reason}, event ${event.event_id})`,
1969
+ );
1970
+ } catch (err) {
1971
+ logger.error(`mtc federation propose-revoke failed: ${err.message}`);
1972
+ process.exit(1);
1973
+ }
1974
+ });
1975
+
1976
+ // mtc federation confirm-revoke
1977
+ fed
1978
+ .command("confirm-revoke <federation-id> <target-member-id>")
1979
+ .description("Confirm a previously-proposed revoke (after grace period)")
1980
+ .requiredOption("--actor <member-id>", "Confirming member")
1981
+ .option(
1982
+ "--reason <text>",
1983
+ "Confirmation reason (key-compromise → mark key compromised)",
1984
+ )
1985
+ .option(
1986
+ "--no-quorum-check",
1987
+ "Skip the pre-flight check that a matching propose-revoke exists (caller assumes responsibility)",
1988
+ )
1989
+ .option("--json", "JSON output")
1990
+ .action((federationId, targetMemberId, options) => {
1991
+ try {
1992
+ if (options.quorumCheck !== false) {
1993
+ requireOpenProposal(
1994
+ federationId,
1995
+ "propose-revoke",
1996
+ (p) => p.target_member_id === targetMemberId,
1997
+ );
1998
+ }
1999
+ const { keys, alg } = loadMemberSigner(federationId, options.actor);
2000
+ const event = emitAndPersist(federationId, {
2001
+ eventType: "confirm-revoke",
2002
+ actorMemberId: options.actor,
2003
+ secretKey: keys.secretKey,
2004
+ publicKey: keys.publicKey,
2005
+ alg,
2006
+ payload: { target_member_id: targetMemberId, reason: options.reason },
2007
+ });
2008
+ if (options.json) return console.log(JSON.stringify(event, null, 2));
2009
+ logger.success(
2010
+ `Confirmed revoke of ${targetMemberId} (event ${event.event_id})`,
2011
+ );
2012
+ } catch (err) {
2013
+ logger.error(`mtc federation confirm-revoke failed: ${err.message}`);
2014
+ process.exit(1);
2015
+ }
2016
+ });
2017
+
2018
+ // mtc federation rotate-key
2019
+ fed
2020
+ .command("rotate-key <federation-id>")
2021
+ .description("Rotate the actor's signing key to a new pubkey")
2022
+ .requiredOption("--actor <member-id>", "Member rotating their key")
2023
+ .requiredOption("--new-pubkey-id <id>", "New public-key id")
2024
+ .option("--new-alg <alg>", "ed25519 | slh-dsa-128f")
2025
+ .option("--json", "JSON output")
2026
+ .action((federationId, options) => {
2027
+ try {
2028
+ const { keys, alg } = loadMemberSigner(federationId, options.actor);
2029
+ const event = emitAndPersist(federationId, {
2030
+ eventType: "rotate-key",
2031
+ actorMemberId: options.actor,
2032
+ secretKey: keys.secretKey,
2033
+ publicKey: keys.publicKey,
2034
+ alg,
2035
+ payload: {
2036
+ new_pubkey_id: options.newPubkeyId,
2037
+ new_alg: options.newAlg,
2038
+ },
2039
+ });
2040
+ if (options.json) return console.log(JSON.stringify(event, null, 2));
2041
+ logger.success(
2042
+ `${options.actor} rotated key to ${options.newPubkeyId} (event ${event.event_id})`,
2043
+ );
2044
+ } catch (err) {
2045
+ logger.error(`mtc federation rotate-key failed: ${err.message}`);
2046
+ process.exit(1);
2047
+ }
2048
+ });
2049
+
2050
+ // mtc federation propose-threshold
2051
+ fed
2052
+ .command("propose-threshold <federation-id> <new-threshold>")
2053
+ .description("Propose a new M-of-N threshold (30-day cooldown)")
2054
+ .requiredOption("--actor <member-id>", "Proposing member")
2055
+ .option("--json", "JSON output")
2056
+ .action((federationId, newThreshold, options) => {
2057
+ try {
2058
+ const target = parseInt(newThreshold, 10);
2059
+ if (!Number.isInteger(target) || target < 1) {
2060
+ throw new Error("new-threshold must be a positive integer");
2061
+ }
2062
+ const { keys, alg } = loadMemberSigner(federationId, options.actor);
2063
+ const event = emitAndPersist(federationId, {
2064
+ eventType: "propose-threshold",
2065
+ actorMemberId: options.actor,
2066
+ secretKey: keys.secretKey,
2067
+ publicKey: keys.publicKey,
2068
+ alg,
2069
+ payload: { proposed_threshold: target },
2070
+ });
2071
+ if (options.json) return console.log(JSON.stringify(event, null, 2));
2072
+ logger.success(
2073
+ `Proposed threshold ${target} (event ${event.event_id}, 30-day cooldown)`,
2074
+ );
2075
+ } catch (err) {
2076
+ logger.error(`mtc federation propose-threshold failed: ${err.message}`);
2077
+ process.exit(1);
2078
+ }
2079
+ });
2080
+
2081
+ // mtc federation confirm-threshold — apply a specific (or most recent) propose-threshold
2082
+ fed
2083
+ .command("confirm-threshold <federation-id>")
2084
+ .description(
2085
+ "Confirm a propose-threshold (default: most recent; --proposal-event-id picks a specific one)",
2086
+ )
2087
+ .requiredOption("--actor <member-id>", "Confirming member")
2088
+ .option(
2089
+ "--proposal-event-id <id>",
2090
+ "Specific propose-threshold event_id (CRDT-style explicit selection when multiple proposals are open)",
2091
+ )
2092
+ .option(
2093
+ "--no-quorum-check",
2094
+ "Skip the pre-flight check that an open propose-threshold exists",
2095
+ )
2096
+ .option("--json", "JSON output")
2097
+ .action((federationId, options) => {
2098
+ try {
2099
+ if (options.quorumCheck !== false) {
2100
+ requireOpenProposal(federationId, "propose-threshold", (p, ev) => {
2101
+ if (!Number.isInteger(p.proposed_threshold)) return false;
2102
+ if (
2103
+ options.proposalEventId &&
2104
+ ev &&
2105
+ ev.event_id !== options.proposalEventId
2106
+ ) {
2107
+ return false;
2108
+ }
2109
+ return true;
2110
+ });
2111
+ }
2112
+ const { keys, alg } = loadMemberSigner(federationId, options.actor);
2113
+ const payload = options.proposalEventId
2114
+ ? { proposal_event_id: options.proposalEventId }
2115
+ : {};
2116
+ const event = emitAndPersist(federationId, {
2117
+ eventType: "confirm-threshold",
2118
+ actorMemberId: options.actor,
2119
+ secretKey: keys.secretKey,
2120
+ publicKey: keys.publicKey,
2121
+ alg,
2122
+ payload,
2123
+ });
2124
+ if (options.json) return console.log(JSON.stringify(event, null, 2));
2125
+ logger.success(
2126
+ `Confirmed${options.proposalEventId ? " specific" : ""} threshold proposal (event ${event.event_id})`,
2127
+ );
2128
+ } catch (err) {
2129
+ logger.error(`mtc federation confirm-threshold failed: ${err.message}`);
2130
+ process.exit(1);
2131
+ }
2132
+ });
2133
+
2134
+ // mtc federation fork
2135
+ fed
2136
+ .command("fork <federation-id> <new-federation-id>")
2137
+ .description(
2138
+ "Spawn a new federation with a subset of current members (members leave the original)",
2139
+ )
2140
+ .requiredOption("--actor <member-id>", "Forking member")
2141
+ .requiredOption(
2142
+ "--members <ids>",
2143
+ "Comma-separated list of member-ids leaving for the new federation",
2144
+ )
2145
+ .option("--json", "JSON output")
2146
+ .action((federationId, newFedId, options) => {
2147
+ try {
2148
+ const memberIds = options.members
2149
+ .split(",")
2150
+ .map((s) => s.trim())
2151
+ .filter(Boolean);
2152
+ const { keys, alg } = loadMemberSigner(federationId, options.actor);
2153
+ const event = emitAndPersist(federationId, {
2154
+ eventType: "fork",
2155
+ actorMemberId: options.actor,
2156
+ secretKey: keys.secretKey,
2157
+ publicKey: keys.publicKey,
2158
+ alg,
2159
+ payload: {
2160
+ new_federation_id: newFedId,
2161
+ member_ids: memberIds,
2162
+ },
2163
+ });
2164
+ if (options.json) return console.log(JSON.stringify(event, null, 2));
2165
+ logger.success(
2166
+ `Forked ${federationId} → ${newFedId} (members: ${memberIds.join(", ")}; event ${event.event_id})`,
2167
+ );
2168
+ } catch (err) {
2169
+ logger.error(`mtc federation fork failed: ${err.message}`);
2170
+ process.exit(1);
2171
+ }
2172
+ });
2173
+
2174
+ // mtc federation merge
2175
+ fed
2176
+ .command("merge <federation-id> <other-federation-id> <new-federation-id>")
2177
+ .description(
2178
+ "Mark this federation as winding down into a new merged federation",
2179
+ )
2180
+ .requiredOption("--actor <member-id>", "Merging member")
2181
+ .option("--json", "JSON output")
2182
+ .action((federationId, otherFedId, newFedId, options) => {
2183
+ try {
2184
+ const { keys, alg } = loadMemberSigner(federationId, options.actor);
2185
+ const event = emitAndPersist(federationId, {
2186
+ eventType: "merge",
2187
+ actorMemberId: options.actor,
2188
+ secretKey: keys.secretKey,
2189
+ publicKey: keys.publicKey,
2190
+ alg,
2191
+ payload: {
2192
+ other_federation_id: otherFedId,
2193
+ new_federation_id: newFedId,
2194
+ },
2195
+ });
2196
+ if (options.json) return console.log(JSON.stringify(event, null, 2));
2197
+ logger.success(
2198
+ `Merged ${federationId} + ${otherFedId} → ${newFedId} (event ${event.event_id})`,
2199
+ );
2200
+ } catch (err) {
2201
+ logger.error(`mtc federation merge failed: ${err.message}`);
2202
+ process.exit(1);
2203
+ }
2204
+ });
2205
+
2206
+ // mtc federation governance-sync-libp2p — pubsub gossipsub variant
2207
+ fed
2208
+ .command("governance-sync-libp2p <federation-id>")
2209
+ .description(
2210
+ "Sync governance events over libp2p gossipsub (topic mtc-federation-governance/v1/<fed>)",
2211
+ )
2212
+ .option("--listen <maddr>", "libp2p listen multiaddr", "/ip4/0.0.0.0/tcp/0")
2213
+ .option(
2214
+ "--connect <maddr>",
2215
+ "Seed peer multiaddr (repeatable)",
2216
+ (v, prev) => (prev ? [...prev, v] : [v]),
2217
+ [],
2218
+ )
2219
+ .option(
2220
+ "--interval <seconds>",
2221
+ "Publish-new-events interval (default: 10)",
2222
+ (v) => parseInt(v, 10),
2223
+ 10,
2224
+ )
2225
+ .option("--verify", "Verify signatures before appending received events")
2226
+ .option(
2227
+ "--once",
2228
+ "Subscribe + publish-once + wait <interval> seconds, then exit (test/cron)",
2229
+ )
2230
+ .option("--json", "Per-tick JSON output")
2231
+ .action(async (federationId, options) => {
2232
+ try {
2233
+ await runGovernanceSyncLibp2p(federationId, options);
2234
+ } catch (err) {
2235
+ logger.error(
2236
+ `mtc federation governance-sync-libp2p failed: ${err.message}`,
2237
+ );
2238
+ process.exit(1);
2239
+ }
2240
+ });
2241
+
2242
+ // mtc federation governance-sync-stats — read live tick stats written by sync daemons
2243
+ fed
2244
+ .command("governance-sync-stats <federation-id>")
2245
+ .description(
2246
+ "Read live sync stats (last tick + cumulative counters) written by governance-sync-serve / governance-sync-libp2p",
2247
+ )
2248
+ .option("--json", "JSON output")
2249
+ .action((federationId, options) => {
2250
+ try {
2251
+ const file = path.join(
2252
+ getFederationDir(),
2253
+ "governance",
2254
+ `${federationId}.sync-stats.json`,
2255
+ );
2256
+ if (!fs.existsSync(file)) {
2257
+ const empty = {
2258
+ federation_id: federationId,
2259
+ stats_file: file,
2260
+ available: false,
2261
+ note: "No sync daemon has written stats yet — start governance-sync-serve or governance-sync-libp2p first.",
2262
+ };
2263
+ if (options.json) return console.log(JSON.stringify(empty, null, 2));
2264
+ logger.warn(empty.note);
2265
+ return;
2266
+ }
2267
+ const data = JSON.parse(fs.readFileSync(file, "utf-8"));
2268
+ if (options.json) return console.log(JSON.stringify(data, null, 2));
2269
+ logger.log(`Federation: ${federationId}`);
2270
+ logger.log(`Last tick: ${data.last_tick_at || "—"}`);
2271
+ logger.log(`Mode: ${data.mode || "—"}`);
2272
+ if (data.publish) {
2273
+ logger.log(
2274
+ `Publish: last=${data.publish.last_published || 0} new (${data.publish.last_skipped || 0} skipped); total=${data.publish.total_published || 0}`,
2275
+ );
2276
+ }
2277
+ if (data.pull) {
2278
+ logger.log(
2279
+ `Pull: last=${data.pull.last_appended || 0} new (dedup=${data.pull.last_duplicates || 0}); total=${data.pull.total_appended || 0}`,
2280
+ );
2281
+ }
2282
+ if (data.libp2p) {
2283
+ logger.log(
2284
+ `Libp2p: wire received=${data.libp2p.wire_received || 0} appended=${data.libp2p.wire_appended || 0} invalid=${data.libp2p.wire_invalid || 0} unknown=${data.libp2p.wire_unknown || 0}`,
2285
+ );
2286
+ }
2287
+ } catch (err) {
2288
+ logger.error(
2289
+ `mtc federation governance-sync-stats failed: ${err.message}`,
2290
+ );
2291
+ process.exit(1);
2292
+ }
2293
+ });
2294
+
2295
+ // mtc federation cross-trust-create — emit a cross-federation trust anchor record (v0.3)
2296
+ fed
2297
+ .command("cross-trust-create <host-fed> <trusted-fed>")
2298
+ .description(
2299
+ "Build a cross-federation trust anchor record (host accepts landmarks from trusted)",
2300
+ )
2301
+ .requiredOption(
2302
+ "--threshold <m>",
2303
+ "Trusted federation's threshold at snapshot time",
2304
+ (v) => parseInt(v, 10),
2305
+ )
2306
+ .requiredOption(
2307
+ "--member <id:pubkey>",
2308
+ "Trusted member entry, id:pubkey_id (repeatable)",
2309
+ (v, prev) => (prev ? [...prev, v] : [v]),
2310
+ [],
2311
+ )
2312
+ .option(
2313
+ "--accepted-kinds <kinds>",
2314
+ "Comma-separated landmark kinds to accept (default: did,skill,bridge,audit)",
2315
+ )
2316
+ .option("--expires-at <iso>", "ISO 8601 expiry (default: 90 days)")
2317
+ .option(
2318
+ "--out <path>",
2319
+ "Write the anchor JSON to a file (otherwise stdout)",
2320
+ )
2321
+ .option("--json", "JSON output (default unless --out given)")
2322
+ .action((hostFed, trustedFed, options) => {
2323
+ try {
2324
+ const roster = (options.member || []).map((entry) => {
2325
+ const [member_id, pubkey_id] = entry.split(":", 2);
2326
+ if (!member_id || !pubkey_id) {
2327
+ throw new Error(
2328
+ `bad --member entry "${entry}", expected id:pubkey_id`,
2329
+ );
2330
+ }
2331
+ return { member_id, pubkey_id, alg: "ed25519" };
2332
+ });
2333
+ const anchor = mtcLib.createCrossFederationTrustAnchor({
2334
+ host_federation_id: hostFed,
2335
+ trusted_federation_id: trustedFed,
2336
+ member_roster_snapshot: roster,
2337
+ threshold: options.threshold,
2338
+ accepted_kinds: options.acceptedKinds
2339
+ ? options.acceptedKinds.split(",").map((s) => s.trim())
2340
+ : undefined,
2341
+ expires_at: options.expiresAt,
2342
+ });
2343
+ const json = JSON.stringify(anchor, null, 2);
2344
+ if (options.out) {
2345
+ fs.mkdirSync(path.dirname(options.out), { recursive: true });
2346
+ fs.writeFileSync(options.out, json, "utf-8");
2347
+ if (options.json) console.log(json);
2348
+ else
2349
+ logger.success(
2350
+ `Cross-fed trust anchor written: ${options.out} (expires ${anchor.expires_at})`,
2351
+ );
2352
+ } else {
2353
+ console.log(json);
2354
+ }
2355
+ } catch (err) {
2356
+ logger.error(
2357
+ `mtc federation cross-trust-create failed: ${err.message}`,
2358
+ );
2359
+ process.exit(1);
2360
+ }
2361
+ });
2362
+
2363
+ // mtc federation cross-trust-validate — validate an anchor's structure + freshness
2364
+ fed
2365
+ .command("cross-trust-validate <anchor-path>")
2366
+ .description("Validate a cross-federation trust anchor JSON file")
2367
+ .option("--json", "JSON output")
2368
+ .action((anchorPath, options) => {
2369
+ try {
2370
+ const anchor = JSON.parse(fs.readFileSync(anchorPath, "utf-8"));
2371
+ const result = mtcLib.validateCrossFederationTrustAnchor(anchor);
2372
+ if (options.json) {
2373
+ console.log(
2374
+ JSON.stringify({ ...result, anchor_path: anchorPath }, null, 2),
2375
+ );
2376
+ if (!result.ok) process.exit(2);
2377
+ return;
2378
+ }
2379
+ if (result.ok) {
2380
+ logger.success(
2381
+ `✓ Anchor valid: ${anchor.host_federation_id} → ${anchor.trusted_federation_id} (expires ${anchor.expires_at})`,
2382
+ );
2383
+ } else {
2384
+ logger.error(`✗ Anchor invalid: ${result.code}`);
2385
+ process.exit(2);
2386
+ }
2387
+ } catch (err) {
2388
+ logger.error(
2389
+ `mtc federation cross-trust-validate failed: ${err.message}`,
2390
+ );
2391
+ process.exit(1);
2392
+ }
2393
+ });
2394
+
2395
+ // mtc federation audit — independent third-party auditor of governance.log (v0.3)
2396
+ fed
2397
+ .command("audit <federation-id>")
2398
+ .description(
2399
+ "Offline audit: replay governance.log + verify each event signature against the rolling roster",
2400
+ )
2401
+ .option("--json", "JSON output (full report incl. final_state)")
2402
+ .option("--summary", "Show only the finding counts + ok/fail")
2403
+ .action((federationId, options) => {
2404
+ try {
2405
+ const events = loadGovernanceLog(federationId);
2406
+ const report = mtcLib.auditGovernanceLog(events, federationId);
2407
+ if (options.json) return console.log(JSON.stringify(report, null, 2));
2408
+ const errorCount = report.findings.filter(
2409
+ (f) => f.severity === "error",
2410
+ ).length;
2411
+ const warnCount = report.findings.filter(
2412
+ (f) => f.severity === "warn",
2413
+ ).length;
2414
+ if (options.summary) {
2415
+ logger.log(
2416
+ `${report.ok ? "✓" : "✗"} ${federationId}: ${report.events_count} events, ${errorCount} errors, ${warnCount} warnings`,
2417
+ );
2418
+ if (!report.ok) process.exit(2);
2419
+ return;
2420
+ }
2421
+ logger.log(`Federation: ${federationId}`);
2422
+ logger.log(`Events: ${report.events_count}`);
2423
+ logger.log(
2424
+ `Audit: ${report.ok ? "✓ PASS" : "✗ FAIL"} (errors=${errorCount}, warnings=${warnCount})`,
2425
+ );
2426
+ if (report.findings.length > 0) {
2427
+ logger.log(`\nFindings:`);
2428
+ for (const f of report.findings) {
2429
+ const sev = f.severity === "error" ? "ERROR" : "WARN ";
2430
+ logger.log(` [${sev}] ${f.code}: ${f.message}`);
2431
+ logger.log(` event_id=${f.event_id}`);
2432
+ }
2433
+ }
2434
+ if (!report.ok) process.exit(2);
2435
+ } catch (err) {
2436
+ logger.error(`mtc federation audit failed: ${err.message}`);
2437
+ process.exit(1);
2438
+ }
2439
+ });
2440
+
2441
+ // v0.3 #2 — On-chain governance anchor (Q-COMP-3 unlocked 2026-05-03)
2442
+ // The CLI ships filesystem-backed mock chain client; production wires
2443
+ // a real ConsortiumChainClient via --chain-impl <module> (future).
2444
+
2445
+ fed
2446
+ .command("governance-anchor <federation-id>")
2447
+ .description(
2448
+ "Compute snapshot hash of governance.log + publish to a chain-anchor store (Q-COMP-3 v0.3 #2)",
2449
+ )
2450
+ .requiredOption("--actor <member-id>", "Anchoring member")
2451
+ .requiredOption(
2452
+ "--chain-store <dir>",
2453
+ "Filesystem dir simulating the chain anchor store (production: --chain-impl swap-in)",
2454
+ )
2455
+ .option("--chain-name <name>", "Chain name label", "consortium-mock")
2456
+ .option("--json", "JSON output")
2457
+ .action(async (federationId, options) => {
2458
+ try {
2459
+ const events = loadGovernanceLog(federationId);
2460
+ const record = mtcLib.buildGovernanceAnchorRecord(
2461
+ events,
2462
+ federationId,
2463
+ options.actor,
2464
+ );
2465
+ const client = new mtcLib.FilesystemChainAnchorClient({
2466
+ rootDir: options.chainStore,
2467
+ chainName: options.chainName,
2468
+ });
2469
+ const receipt = await client.publish(record);
2470
+ const out = {
2471
+ federation_id: federationId,
2472
+ anchored: true,
2473
+ snapshot_hash: record.snapshot_hash,
2474
+ events_count: record.events_count,
2475
+ last_event_id: record.last_event_id,
2476
+ tx_hash: receipt.tx_hash,
2477
+ block_height: receipt.block_height,
2478
+ chain_name: options.chainName,
2479
+ anchored_at: receipt.anchored_at,
2480
+ };
2481
+ if (options.json) return console.log(JSON.stringify(out, null, 2));
2482
+ logger.success(
2483
+ `Anchored ${federationId} snapshot (${record.events_count} events, hash=${record.snapshot_hash}) → tx=${receipt.tx_hash} @ block=${receipt.block_height}`,
2484
+ );
2485
+ } catch (err) {
2486
+ logger.error(`mtc federation governance-anchor failed: ${err.message}`);
2487
+ process.exit(1);
2488
+ }
2489
+ });
2490
+
2491
+ fed
2492
+ .command("governance-verify-anchor <federation-id>")
2493
+ .description(
2494
+ "Fetch latest chain anchor + compare against local governance.log snapshot hash",
2495
+ )
2496
+ .requiredOption(
2497
+ "--chain-store <dir>",
2498
+ "Filesystem dir simulating the chain anchor store",
2499
+ )
2500
+ .option("--json", "JSON output")
2501
+ .action(async (federationId, options) => {
2502
+ try {
2503
+ const client = new mtcLib.FilesystemChainAnchorClient({
2504
+ rootDir: options.chainStore,
2505
+ });
2506
+ const latest = await client.fetchLatest(federationId);
2507
+ if (!latest) {
2508
+ const out = {
2509
+ federation_id: federationId,
2510
+ ok: false,
2511
+ code: "NO_ANCHOR_ON_CHAIN",
2512
+ message:
2513
+ "No anchor record found for this federation in the chain store",
2514
+ };
2515
+ if (options.json) {
2516
+ console.log(JSON.stringify(out, null, 2));
2517
+ process.exit(2);
2518
+ return;
2519
+ }
2520
+ logger.error(out.message);
2521
+ process.exit(2);
2522
+ }
2523
+ const events = loadGovernanceLog(federationId);
2524
+ const result = mtcLib.verifyGovernanceAnchor(latest, events);
2525
+ const out = {
2526
+ federation_id: federationId,
2527
+ ...result,
2528
+ anchor_block_height: latest.block_height,
2529
+ anchor_tx_hash: latest.tx_hash,
2530
+ anchor_anchored_at: latest.anchored_at,
2531
+ };
2532
+ if (options.json) {
2533
+ console.log(JSON.stringify(out, null, 2));
2534
+ if (!result.ok) process.exit(2);
2535
+ return;
2536
+ }
2537
+ if (result.ok) {
2538
+ logger.success(
2539
+ `✓ Anchor matches: ${result.expected_hash} (block=${latest.block_height}, anchored=${latest.anchored_at})`,
2540
+ );
2541
+ } else {
2542
+ logger.error(
2543
+ `✗ Anchor mismatch: ${result.code}\n expected: ${result.expected_hash}\n actual: ${result.actual_hash}`,
2544
+ );
2545
+ if (result.drift) {
2546
+ logger.log(
2547
+ ` drift: events_count_diff=${result.drift.events_count_diff}`,
2548
+ );
2549
+ }
2550
+ process.exit(2);
2551
+ }
2552
+ } catch (err) {
2553
+ logger.error(
2554
+ `mtc federation governance-verify-anchor failed: ${err.message}`,
2555
+ );
2556
+ process.exit(1);
2557
+ }
2558
+ });
2559
+
2560
+ // mtc federation governance-sync-serve — daemon: periodically publish + pull
2561
+ fed
2562
+ .command("governance-sync-serve <federation-id>")
2563
+ .description(
2564
+ "Daemon: periodically publish local governance events + pull remote ones from a shared drop-zone",
2565
+ )
2566
+ .requiredOption("--drop-zone <dir>", "Shared filesystem directory")
2567
+ .option(
2568
+ "--interval <seconds>",
2569
+ "Sync interval (default: 60)",
2570
+ (v) => parseInt(v, 10),
2571
+ 60,
2572
+ )
2573
+ .option(
2574
+ "--verify",
2575
+ "Verify signatures against local registry on pull (default: trust schema only)",
2576
+ )
2577
+ .option("--once", "Sync once and exit (no daemon loop)")
2578
+ .option("--json", "Emit per-tick JSON results to stdout")
2579
+ .action(async (federationId, options) => {
2580
+ const tick = () => {
2581
+ const stamp = new Date().toISOString();
2582
+ try {
2583
+ const pubResult = runGovernancePublish(
2584
+ federationId,
2585
+ options.dropZone,
2586
+ );
2587
+ const pullResult = runGovernancePull(federationId, options.dropZone, {
2588
+ verify: !!options.verify,
2589
+ });
2590
+ // Persist live stats so governance-sync-stats / web GUI can poll
2591
+ const stats = loadStats(federationId);
2592
+ stats.federation_id = federationId;
2593
+ stats.mode = "filesystem";
2594
+ stats.last_tick_at = stamp;
2595
+ stats.publish = stats.publish || { total_published: 0 };
2596
+ stats.publish.last_published = pubResult.published;
2597
+ stats.publish.last_skipped = pubResult.skipped;
2598
+ stats.publish.total_published =
2599
+ (stats.publish.total_published || 0) + pubResult.published;
2600
+ stats.pull = stats.pull || { total_appended: 0 };
2601
+ stats.pull.last_appended = pullResult.appended;
2602
+ stats.pull.last_duplicates = pullResult.duplicates;
2603
+ stats.pull.last_invalid = pullResult.invalid_signature;
2604
+ stats.pull.last_unknown = pullResult.unknown_signer;
2605
+ stats.pull.total_appended =
2606
+ (stats.pull.total_appended || 0) + pullResult.appended;
2607
+ saveStats(federationId, stats);
2608
+
2609
+ if (options.json) {
2610
+ console.log(
2611
+ JSON.stringify(
2612
+ {
2613
+ tick_at: stamp,
2614
+ publish: pubResult,
2615
+ pull: pullResult,
2616
+ },
2617
+ null,
2618
+ 2,
2619
+ ),
2620
+ );
2621
+ } else {
2622
+ console.log(
2623
+ `[${stamp}] publish: ${pubResult.published} new (${pubResult.skipped} skipped) | pull: ${pullResult.appended} new (dedup ${pullResult.duplicates}, invalid ${pullResult.invalid_signature}, unknown ${pullResult.unknown_signer})`,
2624
+ );
2625
+ }
2626
+ } catch (err) {
2627
+ console.error(`[${stamp}] tick error: ${err.message}`);
2628
+ }
2629
+ };
2630
+
2631
+ tick();
2632
+ if (options.once) return;
2633
+
2634
+ console.log(
2635
+ `governance-sync-serve: federation=${federationId} interval=${options.interval}s drop-zone=${options.dropZone}. Ctrl-C to stop.`,
2636
+ );
2637
+ const handle = setInterval(tick, options.interval * 1000);
2638
+ const stop = () => {
2639
+ clearInterval(handle);
2640
+ console.log("governance-sync-serve: stopped.");
2641
+ process.exit(0);
2642
+ };
2643
+ process.on("SIGINT", stop);
2644
+ process.on("SIGTERM", stop);
2645
+ await new Promise(() => {});
2646
+ });
2647
+
2648
+ // mtc federation governance-publish — push local events to a shared drop-zone
2649
+ fed
2650
+ .command("governance-publish <federation-id>")
2651
+ .description(
2652
+ "Publish local governance events to a shared drop-zone (filesystem path / NFS / Syncthing)",
2653
+ )
2654
+ .requiredOption("--drop-zone <dir>", "Shared filesystem directory")
2655
+ .option("--json", "JSON output")
2656
+ .action((federationId, options) => {
2657
+ try {
2658
+ const result = runGovernancePublish(federationId, options.dropZone);
2659
+ if (options.json) return console.log(JSON.stringify(result, null, 2));
2660
+ logger.success(
2661
+ `Published ${result.published} new event(s) (${result.skipped} already in drop-zone) to ${result.drop_zone}`,
2662
+ );
2663
+ } catch (err) {
2664
+ logger.error(
2665
+ `mtc federation governance-publish failed: ${err.message}`,
2666
+ );
2667
+ process.exit(1);
2668
+ }
2669
+ });
2670
+
2671
+ // mtc federation governance-pull — pull events from a shared drop-zone, dedupe + verify, append locally
2672
+ fed
2673
+ .command("governance-pull <federation-id>")
2674
+ .description(
2675
+ "Pull governance events from a shared drop-zone, dedupe by event_id, append new ones to the local log (with optional signature verify)",
2676
+ )
2677
+ .requiredOption("--drop-zone <dir>", "Shared filesystem directory")
2678
+ .option(
2679
+ "--verify",
2680
+ "Verify signatures against local registry before appending (default: trust schema only)",
2681
+ )
2682
+ .option("--json", "JSON output")
2683
+ .action((federationId, options) => {
2684
+ try {
2685
+ // CLI surface keeps the strict "must exist" contract; the daemon
2686
+ // helper treats absent drop-zone as "nothing yet" and returns zeros.
2687
+ const sourceDir = path.join(
2688
+ options.dropZone,
2689
+ "federation-governance",
2690
+ federationId,
2691
+ );
2692
+ if (!fs.existsSync(sourceDir)) {
2693
+ throw new Error(
2694
+ `drop-zone has no events for ${federationId}: ${sourceDir}`,
2695
+ );
2696
+ }
2697
+ const result = runGovernancePull(federationId, options.dropZone, {
2698
+ verify: !!options.verify,
2699
+ });
2700
+ if (options.json) return console.log(JSON.stringify(result, null, 2));
2701
+ logger.success(
2702
+ `Pulled ${result.appended} new event(s) (dedup: ${result.duplicates}, invalid: ${result.invalid_signature}, unknown: ${result.unknown_signer})`,
2703
+ );
2704
+ } catch (err) {
2705
+ logger.error(`mtc federation governance-pull failed: ${err.message}`);
2706
+ process.exit(1);
2707
+ }
2708
+ });
2709
+
2710
+ // mtc federation governance-log — show all events + replayed state
2711
+ fed
2712
+ .command("governance-log <federation-id>")
2713
+ .description("Show governance.log events + current replayed state")
2714
+ .option("--json", "JSON output")
2715
+ .option("--events-only", "Only print events, skip replay state")
2716
+ .action((federationId, options) => {
2717
+ try {
2718
+ const events = loadGovernanceLog(federationId);
2719
+ if (options.eventsOnly) {
2720
+ if (options.json) return console.log(JSON.stringify(events, null, 2));
2721
+ for (const e of events) {
2722
+ logger.log(
2723
+ `${e.issued_at} ${e.event_type.padEnd(20)} actor=${e.actor_member_id} event_id=${e.event_id}`,
2724
+ );
2725
+ }
2726
+ return;
2727
+ }
2728
+ const state = mtcLib.replayGovernanceLog(events, federationId);
2729
+ if (options.json) {
2730
+ return console.log(JSON.stringify({ events, state }, null, 2));
2731
+ }
2732
+ logger.log(
2733
+ `Federation: ${state.federation_id} status=${state.status} threshold=${state.threshold}`,
2734
+ );
2735
+ logger.log(`Members (${state.members.length}):`);
2736
+ for (const m of state.members) {
2737
+ logger.log(
2738
+ ` ${m.member_id.padEnd(20)} weight=${m.weight} status=${m.status} alg=${m.alg}`,
2739
+ );
2740
+ }
2741
+ if (state.pending_invites.length) {
2742
+ logger.log(`Pending invites (${state.pending_invites.length}):`);
2743
+ for (const i of state.pending_invites) {
2744
+ logger.log(
2745
+ ` ${i.member_id} approve=${i.votes.approve.length}/${i.required} reject=${i.votes.reject.length}`,
2746
+ );
2747
+ }
2748
+ }
2749
+ if (state.pending_threshold) {
2750
+ logger.log(
2751
+ `Pending threshold: ${state.pending_threshold.target} (activates ${state.pending_threshold.activates_at})`,
2752
+ );
2753
+ }
2754
+ if (state.archived_keys.length || state.compromised_keys.length) {
2755
+ logger.log(
2756
+ `Archived keys: ${state.archived_keys.length}, compromised: ${state.compromised_keys.length}`,
2757
+ );
2758
+ }
2759
+ } catch (err) {
2760
+ logger.error(`mtc federation governance-log failed: ${err.message}`);
2761
+ process.exit(1);
2762
+ }
2763
+ });
2764
+ }
2765
+
2766
+ // ─────────────────────────────────────────────────────────────────────────
2767
+ // Federation discover daemon (Phase 3.3)
2768
+ // ─────────────────────────────────────────────────────────────────────────
2769
+
2770
+ function getDiscoverAnnouncesDir(dropZone, federationId) {
2771
+ return path.join(dropZone, "federation-announces", federationId);
2772
+ }
2773
+
2774
+ function getDiscoverFilename(announce) {
2775
+ // pubkey_id is "sha256:base64url" — replace : for cross-platform safety
2776
+ const safe = announce.pubkey_id.replace(/[^a-zA-Z0-9_-]/g, "_");
2777
+ return `${safe}.json`;
2778
+ }
2779
+
2780
+ function publishAnnounce(dropZone, announce) {
2781
+ const dir = getDiscoverAnnouncesDir(dropZone, announce.federation_id);
2782
+ fs.mkdirSync(dir, { recursive: true });
2783
+ const filename = getDiscoverFilename(announce);
2784
+ const target = path.join(dir, filename);
2785
+ // Atomic write: tmp + rename
2786
+ const tmp = `${target}.${process.pid}.tmp`;
2787
+ fs.writeFileSync(tmp, JSON.stringify(announce, null, 2), "utf-8");
2788
+ fs.renameSync(tmp, target);
2789
+ return target;
2790
+ }
2791
+
2792
+ function scanDropZone(dropZone, federationId) {
2793
+ const dir = getDiscoverAnnouncesDir(dropZone, federationId);
2794
+ if (!fs.existsSync(dir)) return [];
2795
+ return fs
2796
+ .readdirSync(dir)
2797
+ .filter((n) => n.endsWith(".json"))
2798
+ .map((n) => {
2799
+ const file = path.join(dir, n);
2800
+ try {
2801
+ return { file, announce: JSON.parse(fs.readFileSync(file, "utf-8")) };
2802
+ } catch (err) {
2803
+ return { file, error: err.message };
2804
+ }
2805
+ });
2806
+ }
2807
+
2808
+ /**
2809
+ * Helper: load member key + return {announceBuilder, member} for self-announce.
2810
+ * Used by both filesystem and libp2p paths.
2811
+ */
2812
+ function loadFederationMemberForAnnounce(federationId, memberId, ttlSeconds) {
2813
+ const registry = loadFederationRegistry();
2814
+ const fedEntry = registry.federations[federationId];
2815
+ if (!fedEntry || !fedEntry.members[memberId]) {
2816
+ throw new Error(
2817
+ `not joined as "${memberId}" in federation "${federationId}" — run \`cc mtc federation join ${federationId} --member-id ${memberId}\` first`,
2818
+ );
2819
+ }
2820
+ const member = fedEntry.members[memberId];
2821
+ if (!member.key_file || !fs.existsSync(member.key_file)) {
2822
+ throw new Error(`member key file missing: ${member.key_file}`);
2823
+ }
2824
+ let signerInfo;
2825
+ if (member.alg === "Ed25519") signerInfo = resolveSigner("ed25519");
2826
+ else if (member.alg === "SLH-DSA-SHA2-128F")
2827
+ signerInfo = resolveSigner("slh-dsa-128f");
2828
+ else throw new Error(`unknown member alg: ${member.alg}`);
2829
+ const keys = loadOrGenerateKeyPair(member.key_file, signerInfo);
2830
+
2831
+ return {
2832
+ member,
2833
+ buildAnnounce: () =>
2834
+ mtcLib.createMemberAnnounce({
2835
+ federationId,
2836
+ memberId,
2837
+ issuer: member.issuer,
2838
+ secretKey: keys.secretKey,
2839
+ publicKey: keys.publicKey,
2840
+ signer: signerInfo.signer,
2841
+ ttlSeconds,
2842
+ }),
2843
+ };
2844
+ }
2845
+
2846
+ async function runFederationDiscover(federationId, options) {
2847
+ const transport = (options.transport || "filesystem").toLowerCase();
2848
+ if (transport === "libp2p") {
2849
+ return runFederationDiscoverLibp2p(federationId, options);
2850
+ }
2851
+ if (transport !== "filesystem") {
2852
+ throw new Error(
2853
+ `Unknown --transport: ${options.transport} (supported: filesystem, libp2p)`,
2854
+ );
2855
+ }
2856
+ if (!options.dropZone) {
2857
+ throw new Error("--drop-zone is required when --transport=filesystem");
2858
+ }
2859
+ return runFederationDiscoverFilesystem(federationId, options);
2860
+ }
2861
+
2862
+ async function runFederationDiscoverFilesystem(federationId, options) {
2863
+ const FederationAnnounceCache = mtcLib.FederationAnnounceCache;
2864
+
2865
+ const cache = new FederationAnnounceCache({
2866
+ persistDir: options.cacheDir,
2867
+ });
2868
+
2869
+ // Build self-announce iff --member-id provided
2870
+ let selfAnnounceFn = null;
2871
+ if (options.memberId) {
2872
+ const { buildAnnounce } = loadFederationMemberForAnnounce(
2873
+ federationId,
2874
+ options.memberId,
2875
+ options.ttl,
2876
+ );
2877
+
2878
+ selfAnnounceFn = () => {
2879
+ const ann = buildAnnounce();
2880
+ const written = publishAnnounce(options.dropZone, ann);
2881
+ return { announce: ann, file: written };
2882
+ };
2883
+ }
2884
+
2885
+ function scanAndIngest() {
2886
+ const entries = scanDropZone(options.dropZone, federationId);
2887
+ let accepted = 0;
2888
+ let rejected = 0;
2889
+ const failures = [];
2890
+ for (const e of entries) {
2891
+ if (e.error) {
2892
+ rejected++;
2893
+ failures.push({ file: e.file, code: "PARSE_ERROR" });
2894
+ continue;
2895
+ }
2896
+ const r = cache.ingest(e.announce);
2897
+ if (r.accepted) accepted++;
2898
+ else {
2899
+ rejected++;
2900
+ failures.push({ file: e.file, code: r.reason });
2901
+ }
2902
+ }
2903
+ return { scanned: entries.length, accepted, rejected, failures };
2904
+ }
2905
+
2906
+ function snapshot() {
2907
+ return {
2908
+ federation_id: federationId,
2909
+ drop_zone: options.dropZone,
2910
+ members: cache.listMembers(federationId).map((m) => ({
2911
+ member_id: m.member_id,
2912
+ issuer: m.issuer,
2913
+ alg: m.alg,
2914
+ pubkey_id: m.pubkey_id,
2915
+ announced_at: m.announced_at,
2916
+ ttl_seconds: m.ttl_seconds,
2917
+ })),
2918
+ };
2919
+ }
2920
+
2921
+ // First pass: announce self + scan
2922
+ let selfFile = null;
2923
+ if (selfAnnounceFn) {
2924
+ const r = selfAnnounceFn();
2925
+ selfFile = r.file;
2926
+ }
2927
+ const firstScan = scanAndIngest();
2928
+
2929
+ if (options.once) {
2930
+ const out = {
2931
+ ok: true,
2932
+ federation_id: federationId,
2933
+ self_announce_file: selfFile,
2934
+ scan: firstScan,
2935
+ ...snapshot(),
2936
+ };
2937
+ if (options.json) {
2938
+ console.log(JSON.stringify(out, null, 2));
2939
+ } else {
2940
+ logger.success(
2941
+ `discovered ${out.members.length} member(s) in federation ${federationId}`,
2942
+ );
2943
+ for (const m of out.members) {
2944
+ logger.log(
2945
+ ` · ${chalk.green(m.member_id)} (${m.alg}) ${chalk.gray(m.pubkey_id.slice(0, 18) + "…")}`,
2946
+ );
2947
+ }
2948
+ }
2949
+ return;
2950
+ }
2951
+
2952
+ // Daemon: re-announce + re-scan on intervals
2953
+ const reannounceMs = Math.max(60, Math.floor(options.ttl / 3)) * 1000;
2954
+ const scanMs = Math.max(1, options.scanInterval) * 1000;
2955
+
2956
+ let scanTimer = null;
2957
+ let announceTimer = null;
2958
+ function cleanup() {
2959
+ if (scanTimer) clearInterval(scanTimer);
2960
+ if (announceTimer) clearInterval(announceTimer);
2961
+ process.exit(0);
2962
+ }
2963
+ process.once("SIGINT", cleanup);
2964
+ process.once("SIGTERM", cleanup);
2965
+
2966
+ // Re-entrancy guard: if a scan tick takes longer than scanInterval (large
2967
+ // drop-zone, slow disk), don't let setInterval stack up overlapping ticks.
2968
+ let scanInProgress = false;
2969
+ scanTimer = setInterval(() => {
2970
+ if (scanInProgress) return;
2971
+ scanInProgress = true;
2972
+ try {
2973
+ const r = scanAndIngest();
2974
+ if (options.json) {
2975
+ console.log(
2976
+ JSON.stringify({ tick: "scan", ...r, ...snapshot() }, null, 2),
2977
+ );
2978
+ } else {
2979
+ logger.log(
2980
+ `[${new Date().toISOString()}] scan: ${r.accepted}+/${r.scanned} accepted, ${cache.listMembers(federationId).length} live`,
2981
+ );
2982
+ }
2983
+ } catch (err) {
2984
+ logger.error(`scan failed: ${err.message}`);
2985
+ } finally {
2986
+ scanInProgress = false;
2987
+ }
2988
+ }, scanMs);
2989
+
2990
+ if (selfAnnounceFn) {
2991
+ announceTimer = setInterval(() => {
2992
+ try {
2993
+ selfAnnounceFn();
2994
+ if (!options.json) {
2995
+ logger.log(
2996
+ `[${new Date().toISOString()}] re-announced self in federation ${federationId}`,
2997
+ );
2998
+ }
2999
+ } catch (err) {
3000
+ logger.error(`self-announce failed: ${err.message}`);
3001
+ }
3002
+ }, reannounceMs);
3003
+ }
3004
+
3005
+ logger.success(
3006
+ `federation discover daemon running (drop-zone: ${options.dropZone}, scan: ${options.scanInterval}s, ttl: ${options.ttl}s)${
3007
+ selfAnnounceFn ? `, announcing as ${options.memberId}` : ", listen-only"
3008
+ }`,
3009
+ );
3010
+ await new Promise(() => {});
3011
+ }
3012
+
3013
+ const FEDERATION_TOPIC_PREFIX = "mtc-federation/v1";
3014
+
3015
+ function federationTopic(federationId) {
3016
+ return `${FEDERATION_TOPIC_PREFIX}/${federationId}`;
3017
+ }
3018
+
3019
+ function governanceTopic(federationId) {
3020
+ return `mtc-federation-governance/v1/${federationId}`;
3021
+ }
3022
+
3023
+ /**
3024
+ * libp2p gossipsub-based governance sync (Phase 2 of v0.9 sync work).
3025
+ *
3026
+ * Each peer subscribes to mtc-federation-governance/v1/<fed>; on each tick
3027
+ * the peer publishes any local events that haven't been published yet
3028
+ * (tracked in <governance-dir>/<fed>.libp2p-pos.json — a tiny offset file
3029
+ * mapping event_id → already-published flag). Receivers dedupe + optionally
3030
+ * verify each event before appending to their local jsonl.
3031
+ *
3032
+ * --once mode subscribes, publishes one batch, waits one interval to drain
3033
+ * inbox, then exits — suitable for cron / tests.
3034
+ */
3035
+ async function runGovernanceSyncLibp2p(federationId, options) {
3036
+ const { Libp2pTransport } =
3037
+ await import("@chainlesschain/core-mtc/transports/libp2p");
3038
+
3039
+ const node = await Libp2pTransport.create({
3040
+ listen: options.listen,
3041
+ mode: "gossipsub",
3042
+ });
3043
+
3044
+ const closeOnError = async (err) => {
3045
+ try {
3046
+ await node.close();
3047
+ } catch (_e) {
3048
+ /* ignore */
3049
+ }
3050
+ throw err;
3051
+ };
3052
+
3053
+ try {
3054
+ return await runGovernanceSyncLibp2pInner(federationId, options, node);
3055
+ } catch (err) {
3056
+ return closeOnError(err);
3057
+ }
3058
+ }
3059
+
3060
+ function loadLibp2pPubMarkers(federationId) {
3061
+ const file = path.join(
3062
+ getFederationDir(),
3063
+ "governance",
3064
+ `${federationId}.libp2p-pos.json`,
3065
+ );
3066
+ if (!fs.existsSync(file)) return new Set();
3067
+ try {
3068
+ return new Set(JSON.parse(fs.readFileSync(file, "utf-8")));
3069
+ } catch (_err) {
3070
+ return new Set();
3071
+ }
3072
+ }
3073
+
3074
+ function saveLibp2pPubMarkers(federationId, ids) {
3075
+ const file = path.join(
3076
+ getFederationDir(),
3077
+ "governance",
3078
+ `${federationId}.libp2p-pos.json`,
3079
+ );
3080
+ fs.mkdirSync(path.dirname(file), { recursive: true });
3081
+ fs.writeFileSync(file, JSON.stringify([...ids]), "utf-8");
3082
+ }
3083
+
3084
+ async function runGovernanceSyncLibp2pInner(federationId, options, node) {
3085
+ const topic = governanceTopic(federationId);
3086
+ let received = 0;
3087
+ let appendedFromWire = 0;
3088
+ let invalidFromWire = 0;
3089
+ let unknownFromWire = 0;
3090
+
3091
+ // verify lookup (built once from local registry)
3092
+ let verifyLookup = null;
3093
+ if (options.verify) {
3094
+ const registry = loadFederationRegistry();
3095
+ const fedEntry = registry.federations[federationId] || { members: {} };
3096
+ verifyLookup = (actor) => {
3097
+ const m = fedEntry.members[actor];
3098
+ if (!m || !m.pubkey_jwk) return null;
3099
+ try {
3100
+ return Buffer.from(m.pubkey_jwk.x, "base64url");
3101
+ } catch (_err) {
3102
+ return null;
3103
+ }
3104
+ };
3105
+ }
3106
+
3107
+ // Subscribe + dispatch
3108
+ node.subscribeRaw(topic, (bytes) => {
3109
+ received++;
3110
+ let ev;
3111
+ try {
3112
+ ev = JSON.parse(new TextDecoder().decode(bytes));
3113
+ } catch (_err) {
3114
+ return;
3115
+ }
3116
+ if (!ev || typeof ev.event_id !== "string") return;
3117
+
3118
+ if (verifyLookup) {
3119
+ const result = mtcLib.verifyGovernanceLog([ev], verifyLookup);
3120
+ if (result.invalid.length) {
3121
+ invalidFromWire++;
3122
+ return;
3123
+ }
3124
+ if (result.unknown.length) {
3125
+ unknownFromWire++;
3126
+ return;
3127
+ }
3128
+ }
3129
+
3130
+ // Dedupe vs local log
3131
+ const local = loadGovernanceLog(federationId);
3132
+ if (local.some((e) => e && e.event_id === ev.event_id)) return;
3133
+ appendGovernanceEvent(federationId, ev);
3134
+ appendedFromWire++;
3135
+ });
3136
+
3137
+ // Dial seed peers
3138
+ for (const peer of options.connect || []) {
3139
+ try {
3140
+ await node.dial(peer);
3141
+ } catch (err) {
3142
+ console.warn(`[libp2p] dial ${peer} failed: ${err.message}`);
3143
+ }
3144
+ }
3145
+
3146
+ const publishTick = async () => {
3147
+ const stamp = new Date().toISOString();
3148
+ const local = loadGovernanceLog(federationId);
3149
+ const published = loadLibp2pPubMarkers(federationId);
3150
+ let publishedThisTick = 0;
3151
+ for (const ev of local) {
3152
+ if (!ev || typeof ev.event_id !== "string") continue;
3153
+ if (published.has(ev.event_id)) continue;
3154
+ try {
3155
+ await node.publishRaw(topic, JSON.stringify(ev));
3156
+ published.add(ev.event_id);
3157
+ publishedThisTick++;
3158
+ } catch (err) {
3159
+ console.warn(`[libp2p] publish ${ev.event_id} failed: ${err.message}`);
3160
+ }
3161
+ }
3162
+ if (publishedThisTick > 0) saveLibp2pPubMarkers(federationId, published);
3163
+
3164
+ // Persist live stats so governance-sync-stats / web GUI can poll
3165
+ const stats = loadStats(federationId);
3166
+ stats.federation_id = federationId;
3167
+ stats.mode = "libp2p";
3168
+ stats.last_tick_at = stamp;
3169
+ stats.publish = stats.publish || { total_published: 0 };
3170
+ stats.publish.last_published = publishedThisTick;
3171
+ stats.publish.total_published =
3172
+ (stats.publish.total_published || 0) + publishedThisTick;
3173
+ stats.libp2p = stats.libp2p || {};
3174
+ stats.libp2p.wire_received = received;
3175
+ stats.libp2p.wire_appended = appendedFromWire;
3176
+ stats.libp2p.wire_invalid = invalidFromWire;
3177
+ stats.libp2p.wire_unknown = unknownFromWire;
3178
+ stats.libp2p.topic = topic;
3179
+ saveStats(federationId, stats);
3180
+
3181
+ if (options.json) {
3182
+ console.log(
3183
+ JSON.stringify(
3184
+ {
3185
+ tick_at: stamp,
3186
+ published: publishedThisTick,
3187
+ wire_received: received,
3188
+ wire_appended: appendedFromWire,
3189
+ wire_invalid: invalidFromWire,
3190
+ wire_unknown: unknownFromWire,
3191
+ },
3192
+ null,
3193
+ 2,
3194
+ ),
3195
+ );
3196
+ } else {
3197
+ console.log(
3198
+ `[${stamp}] published ${publishedThisTick} new event(s); wire received=${received} appended=${appendedFromWire} invalid=${invalidFromWire} unknown=${unknownFromWire}`,
3199
+ );
3200
+ }
3201
+ };
3202
+
3203
+ await publishTick();
3204
+
3205
+ if (options.once) {
3206
+ // Wait one interval to drain inbox, then exit cleanly
3207
+ await new Promise((r) => setTimeout(r, options.interval * 1000));
3208
+ await publishTick();
3209
+ await node.close();
3210
+ return;
3211
+ }
3212
+
3213
+ console.log(
3214
+ `governance-sync-libp2p: federation=${federationId} topic=${topic} interval=${options.interval}s. Ctrl-C to stop.`,
3215
+ );
3216
+ const handle = setInterval(publishTick, options.interval * 1000);
3217
+ const stop = async () => {
3218
+ clearInterval(handle);
3219
+ try {
3220
+ await node.close();
3221
+ } catch (_e) {
3222
+ /* ignore */
3223
+ }
3224
+ console.log("governance-sync-libp2p: stopped.");
3225
+ process.exit(0);
3226
+ };
3227
+ process.on("SIGINT", stop);
3228
+ process.on("SIGTERM", stop);
3229
+ await new Promise(() => {});
3230
+ }
3231
+
3232
+ async function runFederationDiscoverLibp2p(federationId, options) {
3233
+ const FederationAnnounceCache = mtcLib.FederationAnnounceCache;
3234
+ const { Libp2pTransport } =
3235
+ await import("@chainlesschain/core-mtc/transports/libp2p");
3236
+
3237
+ const cache = new FederationAnnounceCache({
3238
+ persistDir: options.cacheDir,
3239
+ });
3240
+
3241
+ let selfBuildAnnounce = null;
3242
+ let selfMember = null;
3243
+ if (options.memberId) {
3244
+ const { member, buildAnnounce } = loadFederationMemberForAnnounce(
3245
+ federationId,
3246
+ options.memberId,
3247
+ options.ttl,
3248
+ );
3249
+ selfBuildAnnounce = buildAnnounce;
3250
+ selfMember = member;
3251
+ }
3252
+
3253
+ // Spin up gossipsub libp2p node
3254
+ const node = await Libp2pTransport.create({
3255
+ listen: options.listen,
3256
+ mode: "gossipsub",
3257
+ });
3258
+
3259
+ // Helper: tear down the node on any error path so we don't leak the
3260
+ // libp2p host when initialization throws after node creation.
3261
+ const closeNodeOnError = async (err) => {
3262
+ try {
3263
+ await node.close();
3264
+ } catch (_e) {
3265
+ /* ignore close errors during error cleanup */
3266
+ }
3267
+ throw err;
3268
+ };
3269
+
3270
+ try {
3271
+ return await runFederationDiscoverLibp2pInner(
3272
+ federationId,
3273
+ options,
3274
+ node,
3275
+ cache,
3276
+ selfBuildAnnounce,
3277
+ selfMember,
3278
+ );
3279
+ } catch (err) {
3280
+ return closeNodeOnError(err);
3281
+ }
3282
+ }
3283
+
3284
+ async function runFederationDiscoverLibp2pInner(
3285
+ federationId,
3286
+ options,
3287
+ node,
3288
+ cache,
3289
+ selfBuildAnnounce,
3290
+ selfMember,
3291
+ ) {
3292
+ const topic = federationTopic(federationId);
3293
+
3294
+ // Subscribe + dispatch into cache
3295
+ let bytesReceived = 0;
3296
+ node.subscribeRaw(topic, (bytes) => {
3297
+ bytesReceived++;
3298
+ try {
3299
+ cache.ingest(JSON.parse(new TextDecoder().decode(bytes)));
3300
+ } catch (_err) {
3301
+ /* malformed announce — drop */
3302
+ }
3303
+ });
3304
+
3305
+ // Dial seed peers
3306
+ for (const peer of options.connect || []) {
3307
+ try {
3308
+ await node.connect(peer);
3309
+ } catch (err) {
3310
+ logger.warn(`connect to ${peer} failed: ${err.message}`);
3311
+ }
3312
+ }
3313
+
3314
+ // Mesh formation wait
3315
+ const meshWaitMs = Math.max(0, options.meshWaitMs ?? 1500);
3316
+ if (meshWaitMs > 0) await new Promise((r) => setTimeout(r, meshWaitMs));
3317
+
3318
+ async function publishSelf() {
3319
+ if (!selfBuildAnnounce) return null;
3320
+ const ann = selfBuildAnnounce();
3321
+ const result = await node.publishRaw(topic, JSON.stringify(ann));
3322
+ return { announce: ann, recipients: result.recipients };
3323
+ }
3324
+
3325
+ function snapshot() {
3326
+ return {
3327
+ federation_id: federationId,
3328
+ transport: "libp2p",
3329
+ multiaddrs: node.multiaddrs(),
3330
+ peer_id: node.peerIdString(),
3331
+ members: cache.listMembers(federationId).map((m) => ({
3332
+ member_id: m.member_id,
3333
+ issuer: m.issuer,
3334
+ alg: m.alg,
3335
+ pubkey_id: m.pubkey_id,
3336
+ announced_at: m.announced_at,
3337
+ ttl_seconds: m.ttl_seconds,
3338
+ })),
3339
+ };
3340
+ }
3341
+
3342
+ // First pass: announce self
3343
+ let firstPublish = null;
3344
+ if (selfBuildAnnounce) {
3345
+ firstPublish = await publishSelf();
3346
+ }
3347
+
3348
+ if (options.once) {
3349
+ // Wait briefly for any incoming announces from peers we just dialed
3350
+ if ((options.connect || []).length > 0) {
3351
+ await new Promise((r) => setTimeout(r, 1000));
3352
+ }
3353
+ const out = {
3354
+ ok: true,
3355
+ ...snapshot(),
3356
+ self_announce: firstPublish
3357
+ ? {
3358
+ member_id: options.memberId,
3359
+ issuer: selfMember?.issuer,
3360
+ recipients: firstPublish.recipients,
3361
+ }
3362
+ : null,
3363
+ bytes_received: bytesReceived,
3364
+ };
3365
+ if (options.json) {
3366
+ console.log(JSON.stringify(out, null, 2));
3367
+ } else {
3368
+ logger.success(
3369
+ `libp2p discover: peer_id=${out.peer_id}, ${out.members.length} member(s) cached`,
3370
+ );
3371
+ for (const a of out.multiaddrs) logger.log(` listen: ${a}`);
3372
+ for (const m of out.members) {
3373
+ logger.log(
3374
+ ` · ${chalk.green(m.member_id)} (${m.alg}) ${chalk.gray(m.pubkey_id.slice(0, 18) + "…")}`,
3375
+ );
3376
+ }
3377
+ }
3378
+ await node.close();
3379
+ return;
3380
+ }
3381
+
3382
+ // Daemon: re-announce on TTL/3
3383
+ const reannounceMs = Math.max(60, Math.floor(options.ttl / 3)) * 1000;
3384
+ let announceTimer = null;
3385
+ if (selfBuildAnnounce) {
3386
+ announceTimer = setInterval(async () => {
3387
+ try {
3388
+ await publishSelf();
3389
+ if (!options.json) {
3390
+ logger.log(
3391
+ `[${new Date().toISOString()}] re-announced via libp2p in federation ${federationId}`,
3392
+ );
3393
+ }
3394
+ } catch (err) {
3395
+ logger.error(`self-announce failed: ${err.message}`);
3396
+ }
3397
+ }, reannounceMs);
3398
+ }
3399
+
3400
+ const cleanup = async () => {
3401
+ if (announceTimer) clearInterval(announceTimer);
3402
+ try {
3403
+ await node.close();
3404
+ } catch (_err) {
3405
+ /* ignore */
3406
+ }
3407
+ process.exit(0);
3408
+ };
3409
+ process.once("SIGINT", cleanup);
3410
+ process.once("SIGTERM", cleanup);
3411
+
3412
+ logger.success(
3413
+ `federation discover daemon running (libp2p, peer_id: ${node.peerIdString()})${
3414
+ selfBuildAnnounce
3415
+ ? `, announcing as ${options.memberId} (TTL ${options.ttl}s)`
3416
+ : ", listen-only"
3417
+ }`,
3418
+ );
3419
+ for (const a of node.multiaddrs()) {
3420
+ logger.log(` listen: ${a}`);
3421
+ }
3422
+ await new Promise(() => {});
3423
+ }
3424
+
3425
+ // Internals exported for tests
3426
+ export const _federationInternals = {
3427
+ FEDERATION_REGISTRY_SCHEMA,
3428
+ loadFederationRegistry,
3429
+ saveFederationRegistry,
3430
+ getFederationDir,
3431
+ getFederationRegistryPath,
3432
+ publishAnnounce,
3433
+ scanDropZone,
3434
+ getDiscoverAnnouncesDir,
3435
+ };