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