chainlesschain 0.160.1 → 0.161.3

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 (191) hide show
  1. package/bin/chainlesschain.js +0 -0
  2. package/package.json +6 -6
  3. package/src/assets/web-panel/.build-hash +1 -1
  4. package/src/assets/web-panel/assets/AIOps-FON6GZIt.js +1 -0
  5. package/src/assets/web-panel/assets/{ActionButton-5QD5uzWP.js → ActionButton-Byb_P_P9.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-D1JxsGBN.js → Analytics-CDG1OwOr.js} +2 -2
  7. package/src/assets/web-panel/assets/AppLayout-lM1Y2Wgo.js +1 -0
  8. package/src/assets/web-panel/assets/Audit-p4krng5l.js +1 -0
  9. package/src/assets/web-panel/assets/Backup-G6AQe6k8.js +1 -0
  10. package/src/assets/web-panel/assets/{BaseInput-joQlVauq.js → BaseInput-tUASXi57.js} +1 -1
  11. package/src/assets/web-panel/assets/Chat-B46MHHAs.js +2 -0
  12. package/src/assets/web-panel/assets/{Checkbox-OEOFA9GM.js → Checkbox-CfDbHGdU.js} +1 -1
  13. package/src/assets/web-panel/assets/Codegen-D1mQ03ap.js +1 -0
  14. package/src/assets/web-panel/assets/{Col-BnLUipDp.js → Col-C0wS_dvU.js} +1 -1
  15. package/src/assets/web-panel/assets/Community-CB4KQIpB.js +1 -0
  16. package/src/assets/web-panel/assets/Compact-CuVvz3Zf.js +1 -0
  17. package/src/assets/web-panel/assets/Compliance-D6bnHyjs.js +1 -0
  18. package/src/assets/web-panel/assets/{Cowork-CCgGSKVR.js → Cowork-Dh3yt9pL.js} +2 -2
  19. package/src/assets/web-panel/assets/{Cron-CZ5pjRxn.js → Cron-DiuKXbnA.js} +2 -2
  20. package/src/assets/web-panel/assets/Crosschain-BFgMa5sQ.js +1 -0
  21. package/src/assets/web-panel/assets/{DID-DPZKMApP.js → DID-Dx7CFjrq.js} +2 -2
  22. package/src/assets/web-panel/assets/Dashboard-D3QHFHex.js +3 -0
  23. package/src/assets/web-panel/assets/{Dropdown-CeywCcVQ.js → Dropdown-sMGnnzUo.js} +1 -1
  24. package/src/assets/web-panel/assets/Federation-CGt7CYhN.js +1 -0
  25. package/src/assets/web-panel/assets/{FormItemContext-CFSiPqbu.js → FormItemContext-DuVk39z3.js} +1 -1
  26. package/src/assets/web-panel/assets/{Git-CjVRJDLg.js → Git-glb99UzZ.js} +2 -2
  27. package/src/assets/web-panel/assets/Governance-CQXGee_r.js +1 -0
  28. package/src/assets/web-panel/assets/Inference-Czm-Q3w3.js +1 -0
  29. package/src/assets/web-panel/assets/KnowledgeGraph-DFrr-B78.js +1 -0
  30. package/src/assets/web-panel/assets/{Logs-BD5C-wTx.js → Logs-ChGXdo0Q.js} +2 -2
  31. package/src/assets/web-panel/assets/Marketplace-DW6P2uIY.js +1 -0
  32. package/src/assets/web-panel/assets/{McpTools-x-Tibae-.js → McpTools-ChOwY-wy.js} +2 -2
  33. package/src/assets/web-panel/assets/{Memory-CR8LXq37.js → Memory-vIbbUTel.js} +2 -2
  34. package/src/assets/web-panel/assets/Mtc-60jSuMAz.js +6 -0
  35. package/src/assets/web-panel/assets/Mtc-Cc8OJxe_.css +1 -0
  36. package/src/assets/web-panel/assets/NLProgramming-fRrjhjpW.js +1 -0
  37. package/src/assets/web-panel/assets/{Notes-BYIn2GOe.js → Notes-C0zhC6_B.js} +3 -3
  38. package/src/assets/web-panel/assets/{Organization-CybJTFN9.js → Organization-Bebfm6lF.js} +4 -4
  39. package/src/assets/web-panel/assets/{Overflow-W4YLQ7yY.js → Overflow-D8ZjaRpc.js} +1 -1
  40. package/src/assets/web-panel/assets/{P2P-kVj43R4j.js → P2P-BjgamPxI.js} +2 -2
  41. package/src/assets/web-panel/assets/{Permissions-CfYE4XFJ.js → Permissions-uwoi4HnY.js} +2 -2
  42. package/src/assets/web-panel/assets/Pipeline-C2aHcq6n.js +1 -0
  43. package/src/assets/web-panel/assets/Privacy-lU34k_c5.js +1 -0
  44. package/src/assets/web-panel/assets/{ProjectSettings-cBqrIhNN.js → ProjectSettings-Dy-Iie9L.js} +2 -2
  45. package/src/assets/web-panel/assets/{Projects-BYY38oZd.js → Projects-B4Vt0iiH.js} +2 -2
  46. package/src/assets/web-panel/assets/{Providers-BsS27cWs.js → Providers-Cu9uRsVk.js} +2 -2
  47. package/src/assets/web-panel/assets/QuickAsk-BE29MB0d.js +1 -0
  48. package/src/assets/web-panel/assets/Recommend-DJ9nU8HM.js +1 -0
  49. package/src/assets/web-panel/assets/Reputation-ThffxFwr.js +1 -0
  50. package/src/assets/web-panel/assets/{Row-N-X7EJ3w.js → Row-CqBtO8lC.js} +1 -1
  51. package/src/assets/web-panel/assets/{RssFeed-D9TjnwgF.js → RssFeed-DaxFEUe6.js} +3 -3
  52. package/src/assets/web-panel/assets/Search-4zVFnx_Y.js +1 -0
  53. package/src/assets/web-panel/assets/{Security-DWbFJK10.js → Security-BEfSoaTM.js} +2 -2
  54. package/src/assets/web-panel/assets/{Services-BPUmhVoH.js → Services-DTAaoYJC.js} +2 -2
  55. package/src/assets/web-panel/assets/{Skeleton-Bo5qPHbE.js → Skeleton-CIZErmoY.js} +2 -2
  56. package/src/assets/web-panel/assets/Skills-CacIwBCt.js +1 -0
  57. package/src/assets/web-panel/assets/Sla-Si0g7a0u.js +1 -0
  58. package/src/assets/web-panel/assets/SpeechSettings-H19DqdAG.js +1 -0
  59. package/src/assets/web-panel/assets/SyncSettings-C6cDrwDR.css +1 -0
  60. package/src/assets/web-panel/assets/SyncSettings-bsC4-4rU.js +1 -0
  61. package/src/assets/web-panel/assets/Tasks-Bxlj8OVW.js +1 -0
  62. package/src/assets/web-panel/assets/Templates-B9YsJQcb.js +1 -0
  63. package/src/assets/web-panel/assets/Tenant-D4YncKb7.js +1 -0
  64. package/src/assets/web-panel/assets/Tokens-B2wJjOdI.js +1 -0
  65. package/src/assets/web-panel/assets/{Trigger-Bhjmjsc5.js → Trigger-DU9-3gN9.js} +1 -1
  66. package/src/assets/web-panel/assets/Trust-_yan18qg.js +1 -0
  67. package/src/assets/web-panel/assets/UkeySign-CZ7Xrq96.js +1 -0
  68. package/src/assets/web-panel/assets/VideoEditing-CUgere5i.js +1 -0
  69. package/src/assets/web-panel/assets/{Wallet-dcRAYsdL.js → Wallet-DSnzYVqY.js} +2 -2
  70. package/src/assets/web-panel/assets/{WebAuthn-oqIS5PCi.js → WebAuthn-De4aHZ87.js} +2 -2
  71. package/src/assets/web-panel/assets/WorkflowEditor-C-QNGwia.js +1 -0
  72. package/src/assets/web-panel/assets/chat-DFt6UJUS.js +1 -0
  73. package/src/assets/web-panel/assets/{colors-D2P6CqS5.js → colors-CHuBSydj.js} +1 -1
  74. package/src/assets/web-panel/assets/{compact-item-CG7qutT_.js → compact-item-DtCmGKIm.js} +1 -1
  75. package/src/assets/web-panel/assets/{createContext-y4UPKgbA.js → createContext-Cetup5Fb.js} +1 -1
  76. package/src/assets/web-panel/assets/{hasIn-Butbu9jZ.js → hasIn-Q509tIlQ.js} +1 -1
  77. package/src/assets/web-panel/assets/icons-zXb7lDZs.js +57 -0
  78. package/src/assets/web-panel/assets/{index-BEfvpbz-.js → index-0KFwdHZg.js} +1 -1
  79. package/src/assets/web-panel/assets/{index-Bs9aHxDD.js → index-7kdZ5ox5.js} +1 -1
  80. package/src/assets/web-panel/assets/index-B2f4ltyM.js +65 -0
  81. package/src/assets/web-panel/assets/{index-C_8hWf5_.js → index-B5d7Dkyp.js} +2 -2
  82. package/src/assets/web-panel/assets/index-BDK2Fkgd.js +13 -0
  83. package/src/assets/web-panel/assets/index-BJFH7IlV.js +21 -0
  84. package/src/assets/web-panel/assets/{index-89HJLKZ-.js → index-BTwIwesN.js} +1 -1
  85. package/src/assets/web-panel/assets/index-Bjz1Y3Gs.js +1 -0
  86. package/src/assets/web-panel/assets/index-BlPG_vWg.js +55 -0
  87. package/src/assets/web-panel/assets/{index-BtuwtDUE.js → index-BmHsGJ8F.js} +1 -1
  88. package/src/assets/web-panel/assets/{index-CWh3IxEh.js → index-BvAwpPN9.js} +1 -1
  89. package/src/assets/web-panel/assets/{index-YmGOWX7h.js → index-C2y5MxfU.js} +2 -2
  90. package/src/assets/web-panel/assets/{index-BYZPJS7A.js → index-C61WQXE3.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-BQr8Y0o5.js → index-C7baQwUM.js} +1 -1
  92. package/src/assets/web-panel/assets/{index-B6U6cYUa.js → index-C8agLSzC.js} +8 -8
  93. package/src/assets/web-panel/assets/{index-DdgjeX4z.js → index-CJjwnpac.js} +1 -1
  94. package/src/assets/web-panel/assets/index-CRZ8WjUK.js +1 -0
  95. package/src/assets/web-panel/assets/index-CX7WO0g5.js +12 -0
  96. package/src/assets/web-panel/assets/index-CbQ3AZcJ.js +3 -0
  97. package/src/assets/web-panel/assets/{index-BvJgRWBq.js → index-CeXoNBLu.js} +2 -2
  98. package/src/assets/web-panel/assets/{index-C1ucrJLg.js → index-Ctdt2gEP.js} +1 -1
  99. package/src/assets/web-panel/assets/index-Cxk-8scO.js +1 -0
  100. package/src/assets/web-panel/assets/index-D0oJF6nX.js +7 -0
  101. package/src/assets/web-panel/assets/index-D7Smgwky.js +1 -0
  102. package/src/assets/web-panel/assets/index-DGAGIqDR.js +1 -0
  103. package/src/assets/web-panel/assets/index-DKWvx42k.js +1 -0
  104. package/src/assets/web-panel/assets/{index-Cc77JZKd.js → index-DVQSXYwb.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-vC5cTycG.js → index-DaMTgJkE.js} +2 -2
  106. package/src/assets/web-panel/assets/index-Dd68wDHG.js +12 -0
  107. package/src/assets/web-panel/assets/{index-BCQ0WlB2.js → index-Dem-mhfD.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-CYlDKn3O.js → index-Dq6E-pj8.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-Dx_ZTZo_.js → index-DuQfQVBe.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-DLMJy9pE.js → index-HqJ94PSG.js} +4 -4
  111. package/src/assets/web-panel/assets/{index-B4Jfv4EB.js → index-R5D_-l07.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-DnI4Aq0q.js → index-SR2lFA_W.js} +2 -2
  113. package/src/assets/web-panel/assets/{index-BJN_3RTO.js → index-hSpRlh93.js} +1 -1
  114. package/src/assets/web-panel/assets/index-jfzI94IT.js +1 -0
  115. package/src/assets/web-panel/assets/{index-DrVnyYpX.js → index-nfLO3XrF.js} +1 -1
  116. package/src/assets/web-panel/assets/{initDefaultProps-CZRZ-1bk.js → initDefaultProps-D7iOaKOY.js} +1 -1
  117. package/src/assets/web-panel/assets/{motion-CvU8SiWF.js → motion-CvCFk7sy.js} +1 -1
  118. package/src/assets/web-panel/assets/{move-ipAfWhya.js → move-BVj5YZrV.js} +1 -1
  119. package/src/assets/web-panel/assets/{omit-D6bJEjz9.js → omit-N_JDguMt.js} +1 -1
  120. package/src/assets/web-panel/assets/{pickAttrs-Dpvzf7sL.js → pickAttrs-BWcuLN5j.js} +1 -1
  121. package/src/assets/web-panel/assets/{placementArrow-D_tEolP1.js → placementArrow--1l0FzZx.js} +1 -1
  122. package/src/assets/web-panel/assets/{responsiveObserve-BEFI7neO.js → responsiveObserve-CGX6EYRt.js} +1 -1
  123. package/src/assets/web-panel/assets/{slide-Bte_KOqM.js → slide-B3hJzlWm.js} +1 -1
  124. package/src/assets/web-panel/assets/{statusUtils-K4xaDRuO.js → statusUtils-BJuExZuw.js} +1 -1
  125. package/src/assets/web-panel/assets/{styleChecker-Cl9YgOVY.js → styleChecker-pshnIK0r.js} +1 -1
  126. package/src/assets/web-panel/assets/{useFlexGapSupport-DNstl1wK.js → useFlexGapSupport-3FQZdpBd.js} +1 -1
  127. package/src/assets/web-panel/assets/useFs-BW1piLQM.js +1 -0
  128. package/src/assets/web-panel/assets/{vnode-ChB-8cXr.js → vnode-D98L6HI1.js} +1 -1
  129. package/src/assets/web-panel/assets/{zoom-meTNBulL.js → zoom-DnmMtXgZ.js} +1 -1
  130. package/src/assets/web-panel/index.html +2 -2
  131. package/src/commands/crosschain.js +564 -8
  132. package/src/commands/mtc.js +1334 -0
  133. package/src/lib/config-manager.js +3 -1
  134. package/src/lib/cross-chain-mtc.js +904 -0
  135. package/src/lib/governance-v2-helpers.js +1 -1
  136. package/src/lib/sync-manager.js +119 -25
  137. package/src/assets/web-panel/assets/AIOps-BHiKMxFI.js +0 -1
  138. package/src/assets/web-panel/assets/AppLayout-DykU9tOE.js +0 -1
  139. package/src/assets/web-panel/assets/Audit-TGBqld9c.js +0 -1
  140. package/src/assets/web-panel/assets/Backup-DjpzIwA6.js +0 -1
  141. package/src/assets/web-panel/assets/Chat-9TYfosy-.js +0 -2
  142. package/src/assets/web-panel/assets/Codegen-5H5UgHJu.js +0 -1
  143. package/src/assets/web-panel/assets/Community-BF3R5GAl.js +0 -1
  144. package/src/assets/web-panel/assets/Compact-C1EkTFek.js +0 -1
  145. package/src/assets/web-panel/assets/Compliance-CpP-ODRU.js +0 -1
  146. package/src/assets/web-panel/assets/Crosschain-C0P-5sm3.js +0 -1
  147. package/src/assets/web-panel/assets/Dashboard-G-BDDAov.js +0 -3
  148. package/src/assets/web-panel/assets/Federation-DuFRY867.js +0 -1
  149. package/src/assets/web-panel/assets/Governance-C0lyocJc.js +0 -1
  150. package/src/assets/web-panel/assets/Inference-BVSAexgk.js +0 -1
  151. package/src/assets/web-panel/assets/KnowledgeGraph-SE4jCwIn.js +0 -1
  152. package/src/assets/web-panel/assets/Marketplace-CL93dFBs.js +0 -1
  153. package/src/assets/web-panel/assets/Mtc-C-PfF5B3.css +0 -1
  154. package/src/assets/web-panel/assets/Mtc-CEtRtMcc.js +0 -1
  155. package/src/assets/web-panel/assets/NLProgramming-B09F6gt2.js +0 -1
  156. package/src/assets/web-panel/assets/Pipeline-BVLo32Ak.js +0 -1
  157. package/src/assets/web-panel/assets/Privacy-Efyb3xpJ.js +0 -1
  158. package/src/assets/web-panel/assets/QuickAsk-6FgX9DC6.js +0 -1
  159. package/src/assets/web-panel/assets/Recommend-BvMXwWFN.js +0 -1
  160. package/src/assets/web-panel/assets/Reputation-DmwTtBfl.js +0 -1
  161. package/src/assets/web-panel/assets/Search-Hapv-QkV.js +0 -1
  162. package/src/assets/web-panel/assets/Skills-JJ8uInMW.js +0 -1
  163. package/src/assets/web-panel/assets/Sla-CEDF9zdV.js +0 -1
  164. package/src/assets/web-panel/assets/SpeechSettings-oIoX_vCx.js +0 -1
  165. package/src/assets/web-panel/assets/Tasks-Cx5wgv5Z.js +0 -1
  166. package/src/assets/web-panel/assets/Templates-BomcBlkN.js +0 -1
  167. package/src/assets/web-panel/assets/Tenant-BxSQZUNh.js +0 -1
  168. package/src/assets/web-panel/assets/Tokens-BlPPoB3C.js +0 -1
  169. package/src/assets/web-panel/assets/Trust-Dsjv7rkb.js +0 -1
  170. package/src/assets/web-panel/assets/UkeySign-Cux8_Ib_.js +0 -1
  171. package/src/assets/web-panel/assets/VideoEditing-BsVR1PN8.js +0 -1
  172. package/src/assets/web-panel/assets/WorkflowEditor-C_fYMBvB.js +0 -1
  173. package/src/assets/web-panel/assets/chat-BQ-Nk2XY.js +0 -1
  174. package/src/assets/web-panel/assets/icons-DvZE-RKs.js +0 -57
  175. package/src/assets/web-panel/assets/index-38mVlGHc.js +0 -1
  176. package/src/assets/web-panel/assets/index-B5FRjJMb.js +0 -1
  177. package/src/assets/web-panel/assets/index-B7FV5EnN.js +0 -1
  178. package/src/assets/web-panel/assets/index-BQfow_sh.js +0 -1
  179. package/src/assets/web-panel/assets/index-C1mK1Ga3.js +0 -1
  180. package/src/assets/web-panel/assets/index-C2K61jP8.js +0 -55
  181. package/src/assets/web-panel/assets/index-CAeKBs9n.js +0 -1
  182. package/src/assets/web-panel/assets/index-Ceaxjpqh.js +0 -13
  183. package/src/assets/web-panel/assets/index-Ci6jXp3l.js +0 -7
  184. package/src/assets/web-panel/assets/index-CyqU4Tck.js +0 -65
  185. package/src/assets/web-panel/assets/index-DtNHlrxp.js +0 -1
  186. package/src/assets/web-panel/assets/index-gWmZm8_Q.js +0 -21
  187. package/src/assets/web-panel/assets/index-hSilB_Q-.js +0 -12
  188. package/src/assets/web-panel/assets/index-qXvwlbkq.js +0 -3
  189. package/src/assets/web-panel/assets/index-rIbVsjde.js +0 -12
  190. package/src/assets/web-panel/assets/useFs-BD-YRwbU.js +0 -1
  191. package/src/assets/web-panel/assets/ws-D_5-FRIb.js +0 -1
@@ -1640,6 +1640,1127 @@ function registerFederationCommands(mtc) {
1640
1640
  process.exit(1);
1641
1641
  }
1642
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
+ });
1643
2764
  }
1644
2765
 
1645
2766
  // ─────────────────────────────────────────────────────────────────────────
@@ -1895,6 +3016,219 @@ function federationTopic(federationId) {
1895
3016
  return `${FEDERATION_TOPIC_PREFIX}/${federationId}`;
1896
3017
  }
1897
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
+
1898
3232
  async function runFederationDiscoverLibp2p(federationId, options) {
1899
3233
  const FederationAnnounceCache = mtcLib.FederationAnnounceCache;
1900
3234
  const { Libp2pTransport } =