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.
- package/bin/chainlesschain.js +0 -0
- package/package.json +6 -6
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/AIOps-FON6GZIt.js +1 -0
- package/src/assets/web-panel/assets/{ActionButton-5QD5uzWP.js → ActionButton-Byb_P_P9.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-D1JxsGBN.js → Analytics-CDG1OwOr.js} +2 -2
- package/src/assets/web-panel/assets/AppLayout-lM1Y2Wgo.js +1 -0
- package/src/assets/web-panel/assets/Audit-p4krng5l.js +1 -0
- package/src/assets/web-panel/assets/Backup-G6AQe6k8.js +1 -0
- package/src/assets/web-panel/assets/{BaseInput-joQlVauq.js → BaseInput-tUASXi57.js} +1 -1
- package/src/assets/web-panel/assets/Chat-B46MHHAs.js +2 -0
- package/src/assets/web-panel/assets/{Checkbox-OEOFA9GM.js → Checkbox-CfDbHGdU.js} +1 -1
- package/src/assets/web-panel/assets/Codegen-D1mQ03ap.js +1 -0
- package/src/assets/web-panel/assets/{Col-BnLUipDp.js → Col-C0wS_dvU.js} +1 -1
- package/src/assets/web-panel/assets/Community-CB4KQIpB.js +1 -0
- package/src/assets/web-panel/assets/Compact-CuVvz3Zf.js +1 -0
- package/src/assets/web-panel/assets/Compliance-D6bnHyjs.js +1 -0
- package/src/assets/web-panel/assets/{Cowork-CCgGSKVR.js → Cowork-Dh3yt9pL.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-CZ5pjRxn.js → Cron-DiuKXbnA.js} +2 -2
- package/src/assets/web-panel/assets/Crosschain-BFgMa5sQ.js +1 -0
- package/src/assets/web-panel/assets/{DID-DPZKMApP.js → DID-Dx7CFjrq.js} +2 -2
- package/src/assets/web-panel/assets/Dashboard-D3QHFHex.js +3 -0
- package/src/assets/web-panel/assets/{Dropdown-CeywCcVQ.js → Dropdown-sMGnnzUo.js} +1 -1
- package/src/assets/web-panel/assets/Federation-CGt7CYhN.js +1 -0
- package/src/assets/web-panel/assets/{FormItemContext-CFSiPqbu.js → FormItemContext-DuVk39z3.js} +1 -1
- package/src/assets/web-panel/assets/{Git-CjVRJDLg.js → Git-glb99UzZ.js} +2 -2
- package/src/assets/web-panel/assets/Governance-CQXGee_r.js +1 -0
- package/src/assets/web-panel/assets/Inference-Czm-Q3w3.js +1 -0
- package/src/assets/web-panel/assets/KnowledgeGraph-DFrr-B78.js +1 -0
- package/src/assets/web-panel/assets/{Logs-BD5C-wTx.js → Logs-ChGXdo0Q.js} +2 -2
- package/src/assets/web-panel/assets/Marketplace-DW6P2uIY.js +1 -0
- package/src/assets/web-panel/assets/{McpTools-x-Tibae-.js → McpTools-ChOwY-wy.js} +2 -2
- package/src/assets/web-panel/assets/{Memory-CR8LXq37.js → Memory-vIbbUTel.js} +2 -2
- package/src/assets/web-panel/assets/Mtc-60jSuMAz.js +6 -0
- package/src/assets/web-panel/assets/Mtc-Cc8OJxe_.css +1 -0
- package/src/assets/web-panel/assets/NLProgramming-fRrjhjpW.js +1 -0
- package/src/assets/web-panel/assets/{Notes-BYIn2GOe.js → Notes-C0zhC6_B.js} +3 -3
- package/src/assets/web-panel/assets/{Organization-CybJTFN9.js → Organization-Bebfm6lF.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-W4YLQ7yY.js → Overflow-D8ZjaRpc.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-kVj43R4j.js → P2P-BjgamPxI.js} +2 -2
- package/src/assets/web-panel/assets/{Permissions-CfYE4XFJ.js → Permissions-uwoi4HnY.js} +2 -2
- package/src/assets/web-panel/assets/Pipeline-C2aHcq6n.js +1 -0
- package/src/assets/web-panel/assets/Privacy-lU34k_c5.js +1 -0
- package/src/assets/web-panel/assets/{ProjectSettings-cBqrIhNN.js → ProjectSettings-Dy-Iie9L.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-BYY38oZd.js → Projects-B4Vt0iiH.js} +2 -2
- package/src/assets/web-panel/assets/{Providers-BsS27cWs.js → Providers-Cu9uRsVk.js} +2 -2
- package/src/assets/web-panel/assets/QuickAsk-BE29MB0d.js +1 -0
- package/src/assets/web-panel/assets/Recommend-DJ9nU8HM.js +1 -0
- package/src/assets/web-panel/assets/Reputation-ThffxFwr.js +1 -0
- package/src/assets/web-panel/assets/{Row-N-X7EJ3w.js → Row-CqBtO8lC.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-D9TjnwgF.js → RssFeed-DaxFEUe6.js} +3 -3
- package/src/assets/web-panel/assets/Search-4zVFnx_Y.js +1 -0
- package/src/assets/web-panel/assets/{Security-DWbFJK10.js → Security-BEfSoaTM.js} +2 -2
- package/src/assets/web-panel/assets/{Services-BPUmhVoH.js → Services-DTAaoYJC.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-Bo5qPHbE.js → Skeleton-CIZErmoY.js} +2 -2
- package/src/assets/web-panel/assets/Skills-CacIwBCt.js +1 -0
- package/src/assets/web-panel/assets/Sla-Si0g7a0u.js +1 -0
- package/src/assets/web-panel/assets/SpeechSettings-H19DqdAG.js +1 -0
- package/src/assets/web-panel/assets/SyncSettings-C6cDrwDR.css +1 -0
- package/src/assets/web-panel/assets/SyncSettings-bsC4-4rU.js +1 -0
- package/src/assets/web-panel/assets/Tasks-Bxlj8OVW.js +1 -0
- package/src/assets/web-panel/assets/Templates-B9YsJQcb.js +1 -0
- package/src/assets/web-panel/assets/Tenant-D4YncKb7.js +1 -0
- package/src/assets/web-panel/assets/Tokens-B2wJjOdI.js +1 -0
- package/src/assets/web-panel/assets/{Trigger-Bhjmjsc5.js → Trigger-DU9-3gN9.js} +1 -1
- package/src/assets/web-panel/assets/Trust-_yan18qg.js +1 -0
- package/src/assets/web-panel/assets/UkeySign-CZ7Xrq96.js +1 -0
- package/src/assets/web-panel/assets/VideoEditing-CUgere5i.js +1 -0
- package/src/assets/web-panel/assets/{Wallet-dcRAYsdL.js → Wallet-DSnzYVqY.js} +2 -2
- package/src/assets/web-panel/assets/{WebAuthn-oqIS5PCi.js → WebAuthn-De4aHZ87.js} +2 -2
- package/src/assets/web-panel/assets/WorkflowEditor-C-QNGwia.js +1 -0
- package/src/assets/web-panel/assets/chat-DFt6UJUS.js +1 -0
- package/src/assets/web-panel/assets/{colors-D2P6CqS5.js → colors-CHuBSydj.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-CG7qutT_.js → compact-item-DtCmGKIm.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-y4UPKgbA.js → createContext-Cetup5Fb.js} +1 -1
- package/src/assets/web-panel/assets/{hasIn-Butbu9jZ.js → hasIn-Q509tIlQ.js} +1 -1
- package/src/assets/web-panel/assets/icons-zXb7lDZs.js +57 -0
- package/src/assets/web-panel/assets/{index-BEfvpbz-.js → index-0KFwdHZg.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bs9aHxDD.js → index-7kdZ5ox5.js} +1 -1
- package/src/assets/web-panel/assets/index-B2f4ltyM.js +65 -0
- package/src/assets/web-panel/assets/{index-C_8hWf5_.js → index-B5d7Dkyp.js} +2 -2
- package/src/assets/web-panel/assets/index-BDK2Fkgd.js +13 -0
- package/src/assets/web-panel/assets/index-BJFH7IlV.js +21 -0
- package/src/assets/web-panel/assets/{index-89HJLKZ-.js → index-BTwIwesN.js} +1 -1
- package/src/assets/web-panel/assets/index-Bjz1Y3Gs.js +1 -0
- package/src/assets/web-panel/assets/index-BlPG_vWg.js +55 -0
- package/src/assets/web-panel/assets/{index-BtuwtDUE.js → index-BmHsGJ8F.js} +1 -1
- package/src/assets/web-panel/assets/{index-CWh3IxEh.js → index-BvAwpPN9.js} +1 -1
- package/src/assets/web-panel/assets/{index-YmGOWX7h.js → index-C2y5MxfU.js} +2 -2
- package/src/assets/web-panel/assets/{index-BYZPJS7A.js → index-C61WQXE3.js} +1 -1
- package/src/assets/web-panel/assets/{index-BQr8Y0o5.js → index-C7baQwUM.js} +1 -1
- package/src/assets/web-panel/assets/{index-B6U6cYUa.js → index-C8agLSzC.js} +8 -8
- package/src/assets/web-panel/assets/{index-DdgjeX4z.js → index-CJjwnpac.js} +1 -1
- package/src/assets/web-panel/assets/index-CRZ8WjUK.js +1 -0
- package/src/assets/web-panel/assets/index-CX7WO0g5.js +12 -0
- package/src/assets/web-panel/assets/index-CbQ3AZcJ.js +3 -0
- package/src/assets/web-panel/assets/{index-BvJgRWBq.js → index-CeXoNBLu.js} +2 -2
- package/src/assets/web-panel/assets/{index-C1ucrJLg.js → index-Ctdt2gEP.js} +1 -1
- package/src/assets/web-panel/assets/index-Cxk-8scO.js +1 -0
- package/src/assets/web-panel/assets/index-D0oJF6nX.js +7 -0
- package/src/assets/web-panel/assets/index-D7Smgwky.js +1 -0
- package/src/assets/web-panel/assets/index-DGAGIqDR.js +1 -0
- package/src/assets/web-panel/assets/index-DKWvx42k.js +1 -0
- package/src/assets/web-panel/assets/{index-Cc77JZKd.js → index-DVQSXYwb.js} +1 -1
- package/src/assets/web-panel/assets/{index-vC5cTycG.js → index-DaMTgJkE.js} +2 -2
- package/src/assets/web-panel/assets/index-Dd68wDHG.js +12 -0
- package/src/assets/web-panel/assets/{index-BCQ0WlB2.js → index-Dem-mhfD.js} +1 -1
- package/src/assets/web-panel/assets/{index-CYlDKn3O.js → index-Dq6E-pj8.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dx_ZTZo_.js → index-DuQfQVBe.js} +1 -1
- package/src/assets/web-panel/assets/{index-DLMJy9pE.js → index-HqJ94PSG.js} +4 -4
- package/src/assets/web-panel/assets/{index-B4Jfv4EB.js → index-R5D_-l07.js} +1 -1
- package/src/assets/web-panel/assets/{index-DnI4Aq0q.js → index-SR2lFA_W.js} +2 -2
- package/src/assets/web-panel/assets/{index-BJN_3RTO.js → index-hSpRlh93.js} +1 -1
- package/src/assets/web-panel/assets/index-jfzI94IT.js +1 -0
- package/src/assets/web-panel/assets/{index-DrVnyYpX.js → index-nfLO3XrF.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-CZRZ-1bk.js → initDefaultProps-D7iOaKOY.js} +1 -1
- package/src/assets/web-panel/assets/{motion-CvU8SiWF.js → motion-CvCFk7sy.js} +1 -1
- package/src/assets/web-panel/assets/{move-ipAfWhya.js → move-BVj5YZrV.js} +1 -1
- package/src/assets/web-panel/assets/{omit-D6bJEjz9.js → omit-N_JDguMt.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-Dpvzf7sL.js → pickAttrs-BWcuLN5j.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-D_tEolP1.js → placementArrow--1l0FzZx.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-BEFI7neO.js → responsiveObserve-CGX6EYRt.js} +1 -1
- package/src/assets/web-panel/assets/{slide-Bte_KOqM.js → slide-B3hJzlWm.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-K4xaDRuO.js → statusUtils-BJuExZuw.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-Cl9YgOVY.js → styleChecker-pshnIK0r.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-DNstl1wK.js → useFlexGapSupport-3FQZdpBd.js} +1 -1
- package/src/assets/web-panel/assets/useFs-BW1piLQM.js +1 -0
- package/src/assets/web-panel/assets/{vnode-ChB-8cXr.js → vnode-D98L6HI1.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-meTNBulL.js → zoom-DnmMtXgZ.js} +1 -1
- package/src/assets/web-panel/index.html +2 -2
- package/src/commands/crosschain.js +564 -8
- package/src/commands/mtc.js +1334 -0
- package/src/lib/config-manager.js +3 -1
- package/src/lib/cross-chain-mtc.js +904 -0
- package/src/lib/governance-v2-helpers.js +1 -1
- package/src/lib/sync-manager.js +119 -25
- package/src/assets/web-panel/assets/AIOps-BHiKMxFI.js +0 -1
- package/src/assets/web-panel/assets/AppLayout-DykU9tOE.js +0 -1
- package/src/assets/web-panel/assets/Audit-TGBqld9c.js +0 -1
- package/src/assets/web-panel/assets/Backup-DjpzIwA6.js +0 -1
- package/src/assets/web-panel/assets/Chat-9TYfosy-.js +0 -2
- package/src/assets/web-panel/assets/Codegen-5H5UgHJu.js +0 -1
- package/src/assets/web-panel/assets/Community-BF3R5GAl.js +0 -1
- package/src/assets/web-panel/assets/Compact-C1EkTFek.js +0 -1
- package/src/assets/web-panel/assets/Compliance-CpP-ODRU.js +0 -1
- package/src/assets/web-panel/assets/Crosschain-C0P-5sm3.js +0 -1
- package/src/assets/web-panel/assets/Dashboard-G-BDDAov.js +0 -3
- package/src/assets/web-panel/assets/Federation-DuFRY867.js +0 -1
- package/src/assets/web-panel/assets/Governance-C0lyocJc.js +0 -1
- package/src/assets/web-panel/assets/Inference-BVSAexgk.js +0 -1
- package/src/assets/web-panel/assets/KnowledgeGraph-SE4jCwIn.js +0 -1
- package/src/assets/web-panel/assets/Marketplace-CL93dFBs.js +0 -1
- package/src/assets/web-panel/assets/Mtc-C-PfF5B3.css +0 -1
- package/src/assets/web-panel/assets/Mtc-CEtRtMcc.js +0 -1
- package/src/assets/web-panel/assets/NLProgramming-B09F6gt2.js +0 -1
- package/src/assets/web-panel/assets/Pipeline-BVLo32Ak.js +0 -1
- package/src/assets/web-panel/assets/Privacy-Efyb3xpJ.js +0 -1
- package/src/assets/web-panel/assets/QuickAsk-6FgX9DC6.js +0 -1
- package/src/assets/web-panel/assets/Recommend-BvMXwWFN.js +0 -1
- package/src/assets/web-panel/assets/Reputation-DmwTtBfl.js +0 -1
- package/src/assets/web-panel/assets/Search-Hapv-QkV.js +0 -1
- package/src/assets/web-panel/assets/Skills-JJ8uInMW.js +0 -1
- package/src/assets/web-panel/assets/Sla-CEDF9zdV.js +0 -1
- package/src/assets/web-panel/assets/SpeechSettings-oIoX_vCx.js +0 -1
- package/src/assets/web-panel/assets/Tasks-Cx5wgv5Z.js +0 -1
- package/src/assets/web-panel/assets/Templates-BomcBlkN.js +0 -1
- package/src/assets/web-panel/assets/Tenant-BxSQZUNh.js +0 -1
- package/src/assets/web-panel/assets/Tokens-BlPPoB3C.js +0 -1
- package/src/assets/web-panel/assets/Trust-Dsjv7rkb.js +0 -1
- package/src/assets/web-panel/assets/UkeySign-Cux8_Ib_.js +0 -1
- package/src/assets/web-panel/assets/VideoEditing-BsVR1PN8.js +0 -1
- package/src/assets/web-panel/assets/WorkflowEditor-C_fYMBvB.js +0 -1
- package/src/assets/web-panel/assets/chat-BQ-Nk2XY.js +0 -1
- package/src/assets/web-panel/assets/icons-DvZE-RKs.js +0 -57
- package/src/assets/web-panel/assets/index-38mVlGHc.js +0 -1
- package/src/assets/web-panel/assets/index-B5FRjJMb.js +0 -1
- package/src/assets/web-panel/assets/index-B7FV5EnN.js +0 -1
- package/src/assets/web-panel/assets/index-BQfow_sh.js +0 -1
- package/src/assets/web-panel/assets/index-C1mK1Ga3.js +0 -1
- package/src/assets/web-panel/assets/index-C2K61jP8.js +0 -55
- package/src/assets/web-panel/assets/index-CAeKBs9n.js +0 -1
- package/src/assets/web-panel/assets/index-Ceaxjpqh.js +0 -13
- package/src/assets/web-panel/assets/index-Ci6jXp3l.js +0 -7
- package/src/assets/web-panel/assets/index-CyqU4Tck.js +0 -65
- package/src/assets/web-panel/assets/index-DtNHlrxp.js +0 -1
- package/src/assets/web-panel/assets/index-gWmZm8_Q.js +0 -21
- package/src/assets/web-panel/assets/index-hSilB_Q-.js +0 -12
- package/src/assets/web-panel/assets/index-qXvwlbkq.js +0 -3
- package/src/assets/web-panel/assets/index-rIbVsjde.js +0 -12
- package/src/assets/web-panel/assets/useFs-BD-YRwbU.js +0 -1
- package/src/assets/web-panel/assets/ws-D_5-FRIb.js +0 -1
package/src/commands/mtc.js
CHANGED
|
@@ -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 } =
|