chainlesschain 0.156.7 → 0.157.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/README.md +2 -2
  2. package/bin/chainlesschain.js +13 -0
  3. package/package.json +3 -2
  4. package/src/assets/web-panel/.build-hash +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-Cs4QdjYb.js → ActionButton-Dp5EHuSs.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-Xot0e9TT.js → Analytics-Dt69R7IX.js} +1 -1
  7. package/src/assets/web-panel/assets/AppLayout-BiOlkA-r.js +1 -0
  8. package/src/assets/web-panel/assets/AppLayout-Wr63FaJ2.css +1 -0
  9. package/src/assets/web-panel/assets/{Backup-Cih5dXcD.js → Backup-sEweroFg.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-Tg40P4JM.js → BaseInput-CMHSpW6z.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-CRfTuSl8.js → Chat-DQusK0UJ.js} +2 -2
  12. package/src/assets/web-panel/assets/{Checkbox-CheA2Ety.js → Checkbox-DXQE64jQ.js} +1 -1
  13. package/src/assets/web-panel/assets/{Col-Cdfsmnaq.js → Col-GesfSTFv.js} +1 -1
  14. package/src/assets/web-panel/assets/{Compact-D3LSgEpW.js → Compact-DOVOXT5H.js} +1 -1
  15. package/src/assets/web-panel/assets/{Cowork-BLRUoSoO.js → Cowork-BaJ4irYi.js} +3 -3
  16. package/src/assets/web-panel/assets/{Cron-9MV6k-MV.js → Cron-DLx6zElQ.js} +2 -2
  17. package/src/assets/web-panel/assets/DID-BDvsVa08.css +1 -0
  18. package/src/assets/web-panel/assets/DID-O2dY9RPB.js +2 -0
  19. package/src/assets/web-panel/assets/Dashboard-BReDcgB_.js +3 -0
  20. package/src/assets/web-panel/assets/{Dropdown-DZxUTZvw.js → Dropdown-O-lFvQpB.js} +1 -1
  21. package/src/assets/web-panel/assets/{FormItemContext-C3_-j_SR.js → FormItemContext-Dld2C8JQ.js} +1 -1
  22. package/src/assets/web-panel/assets/{Git-Cw-gW-kh.js → Git-hvFWgySH.js} +2 -2
  23. package/src/assets/web-panel/assets/KnowledgeGraph-B_y3Ap4i.js +19 -0
  24. package/src/assets/web-panel/assets/KnowledgeGraph-U8ps3aGJ.css +1 -0
  25. package/src/assets/web-panel/assets/{Logs-0SXs6Eyx.js → Logs-DhFPaoDT.js} +1 -1
  26. package/src/assets/web-panel/assets/{McpTools-VVSCkpV2.js → McpTools-D0XTOWu9.js} +2 -2
  27. package/src/assets/web-panel/assets/{Memory-CMweTJyn.js → Memory-DW_B6rnE.js} +2 -2
  28. package/src/assets/web-panel/assets/{Notes-B_W3BfZF.js → Notes-BbyhsweS.js} +2 -2
  29. package/src/assets/web-panel/assets/{Organization-Dz_jGbAM.js → Organization-CChT-ntv.js} +3 -3
  30. package/src/assets/web-panel/assets/{Overflow-Dka3nWV9.js → Overflow-CZx2xJzC.js} +1 -1
  31. package/src/assets/web-panel/assets/{OverrideContext-7M2Kv4Ru.js → OverrideContext-9ePmgwvW.js} +1 -1
  32. package/src/assets/web-panel/assets/{P2P-DGPIG-9j.js → P2P-CwaXSJZR.js} +2 -2
  33. package/src/assets/web-panel/assets/{Permissions-DvXVIlHX.js → Permissions-D5U7ha3z.js} +4 -4
  34. package/src/assets/web-panel/assets/Portal-DXIqogG2.js +1 -0
  35. package/src/assets/web-panel/assets/ProjectSettings-BQX5tF8E.js +2 -0
  36. package/src/assets/web-panel/assets/ProjectSettings-pLSae-wy.css +1 -0
  37. package/src/assets/web-panel/assets/{Projects-Bzn-dJ59.js → Projects-CLL9WS2z.js} +2 -2
  38. package/src/assets/web-panel/assets/{Providers-DUfX_ynl.js → Providers-fjohdAMK.js} +1 -1
  39. package/src/assets/web-panel/assets/{Row-DZhDSo2Q.js → Row-C-bX9EFG.js} +1 -1
  40. package/src/assets/web-panel/assets/{RssFeed-CHQpUl3h.js → RssFeed-BsKQE8pe.js} +3 -3
  41. package/src/assets/web-panel/assets/{Security-FUSOn89T.js → Security-DXW6YBaw.js} +3 -3
  42. package/src/assets/web-panel/assets/{Services-CDh7r75R.js → Services-BnNYoCwx.js} +2 -2
  43. package/src/assets/web-panel/assets/{Skeleton-DxmZ7zRw.js → Skeleton-B-K9_08N.js} +1 -1
  44. package/src/assets/web-panel/assets/{Skills-Dk9Cp1NG.js → Skills-DB8Qba_i.js} +1 -1
  45. package/src/assets/web-panel/assets/{Tasks-CfHL1NrP.js → Tasks-0fHss8Sn.js} +1 -1
  46. package/src/assets/web-panel/assets/{Templates-BVbmyn38.js → Templates-Cfch0kkR.js} +1 -1
  47. package/src/assets/web-panel/assets/Trigger-CAZQ8LeJ.js +1 -0
  48. package/src/assets/web-panel/assets/{VideoEditing-BUWYQv2y.js → VideoEditing-CiTgOTo4.js} +1 -1
  49. package/src/assets/web-panel/assets/{Wallet-BDYdEwFf.js → Wallet-BQiQ4WyF.js} +4 -4
  50. package/src/assets/web-panel/assets/{WebAuthn-CvpuagtK.js → WebAuthn-Dq27qj5Y.js} +5 -5
  51. package/src/assets/web-panel/assets/{WorkflowEditor-BR7W5cjw.js → WorkflowEditor-BSXLy_fi.js} +1 -1
  52. package/src/assets/web-panel/assets/{chat-B2uGA8wN.js → chat-BNXQJRrX.js} +1 -1
  53. package/src/assets/web-panel/assets/{collapseMotion-DnZigkzG.js → collapseMotion-CSS8MlIE.js} +1 -1
  54. package/src/assets/web-panel/assets/{colors-C5kDbQCi.js → colors-CHcJ8oqQ.js} +1 -1
  55. package/src/assets/web-panel/assets/{compact-item-Bo_1zDrX.js → compact-item-BIM24cFh.js} +1 -1
  56. package/src/assets/web-panel/assets/{createContext-CniPpJsG.js → createContext-yGfIqyCQ.js} +1 -1
  57. package/src/assets/web-panel/assets/{hasIn-ClDc6Sz8.js → hasIn-BXMsXiz7.js} +1 -1
  58. package/src/assets/web-panel/assets/icons-DEoqYzGZ.js +57 -0
  59. package/src/assets/web-panel/assets/{index-Ch5mAXeh.js → index-36fB-tdr.js} +3 -3
  60. package/src/assets/web-panel/assets/index-B-tWHvIW.js +1 -0
  61. package/src/assets/web-panel/assets/{index-CCdb36il.js → index-B50P9i-m.js} +1 -1
  62. package/src/assets/web-panel/assets/{index-DSiHmo4b.js → index-BDzwtAvJ.js} +1 -1
  63. package/src/assets/web-panel/assets/{index-BUTCJTbj.js → index-BEPQ53hT.js} +1 -1
  64. package/src/assets/web-panel/assets/{index-C92K4iDE.js → index-BPRyVdxV.js} +1 -1
  65. package/src/assets/web-panel/assets/index-BPln_4T8.js +1 -0
  66. package/src/assets/web-panel/assets/index-BPzN5a2A.js +36 -0
  67. package/src/assets/web-panel/assets/{index-CMcGcbea.js → index-BaV1qPw2.js} +2 -2
  68. package/src/assets/web-panel/assets/index-Bm7xqsYQ.js +1 -0
  69. package/src/assets/web-panel/assets/{index-cIgCeEqo.js → index-BsSnr41N.js} +1 -1
  70. package/src/assets/web-panel/assets/{index--lcO-bOn.js → index-BtfOEVOq.js} +2 -2
  71. package/src/assets/web-panel/assets/{index-BCXFoTAw.js → index-C6RUQfzA.js} +1 -1
  72. package/src/assets/web-panel/assets/index-CEBBaWRc.js +1 -0
  73. package/src/assets/web-panel/assets/{index-DTYnvYqB.js → index-CELIaC07.js} +2 -2
  74. package/src/assets/web-panel/assets/{index-BAlSSCbs.js → index-CN1O5Ka0.js} +1 -1
  75. package/src/assets/web-panel/assets/index-CPzBvwi4.js +1 -0
  76. package/src/assets/web-panel/assets/{index-B8y0NO-M.js → index-CSzJBDW_.js} +1 -1
  77. package/src/assets/web-panel/assets/{index-CeSV8f3b.js → index-CWRVLGUY.js} +3 -3
  78. package/src/assets/web-panel/assets/{index-D2fe9a6f.js → index-CWhMnsWX.js} +5 -5
  79. package/src/assets/web-panel/assets/{index-DnQkqOZj.js → index-C_VBCJdj.js} +1 -1
  80. package/src/assets/web-panel/assets/{index-Dtfrhky9.js → index-CfXwGGJa.js} +2 -2
  81. package/src/assets/web-panel/assets/{index-DkSNIJhM.js → index-Csq1ymAd.js} +1 -1
  82. package/src/assets/web-panel/assets/{index-BHruTebo.js → index-CvI-6roc.js} +2 -2
  83. package/src/assets/web-panel/assets/{index-DNY0K7iI.js → index-D2aA1Eww.js} +1 -1
  84. package/src/assets/web-panel/assets/{index-D9bolkbl.js → index-DNwo9Dzx.js} +3 -3
  85. package/src/assets/web-panel/assets/{index-DaLYbr0E.js → index-DViNLv05.js} +1 -1
  86. package/src/assets/web-panel/assets/index-DX3NKnlx.js +1 -0
  87. package/src/assets/web-panel/assets/{index-6tQekF0Y.js → index-DYbkrS1k.js} +1 -1
  88. package/src/assets/web-panel/assets/index-D_E9PBtZ.js +1 -0
  89. package/src/assets/web-panel/assets/{index-vBi4x_6g.js → index-DlqQ3NPc.js} +1 -1
  90. package/src/assets/web-panel/assets/{index-JNbd08FN.js → index-DrBsteza.js} +2 -2
  91. package/src/assets/web-panel/assets/{index-B7nGNm_C.js → index-Mc421_zZ.js} +2 -2
  92. package/src/assets/web-panel/assets/{index-CwhWEkmA.js → index-OcnmMROX.js} +2 -2
  93. package/src/assets/web-panel/assets/{index-Bv2OmZAS.js → index-PbAFYgta.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-CTetsi8W.js → index-T5htsJq3.js} +1 -1
  95. package/src/assets/web-panel/assets/index-lx2gVL_l.js +1 -0
  96. package/src/assets/web-panel/assets/{index-CXxLp7Aw.js → index-pj_DB3gf.js} +4 -4
  97. package/src/assets/web-panel/assets/{initDefaultProps-DMfJaUzk.js → initDefaultProps-DR3dFRrN.js} +1 -1
  98. package/src/assets/web-panel/assets/{motion-sEbWmOWo.js → motion-Dab0Jzue.js} +2 -2
  99. package/src/assets/web-panel/assets/{move-DIWXVs--.js → move-CbXivKK7.js} +1 -1
  100. package/src/assets/web-panel/assets/{omit-D7mkMPhu.js → omit-CyPQ4Rvu.js} +1 -1
  101. package/src/assets/web-panel/assets/{pickAttrs-B25NUX4k.js → pickAttrs-DLkAq0jb.js} +1 -1
  102. package/src/assets/web-panel/assets/{placementArrow-By1Bkq1d.js → placementArrow-OJpzGLKb.js} +1 -1
  103. package/src/assets/web-panel/assets/{responsiveObserve-B3aCQz5r.js → responsiveObserve-BSL-2rcG.js} +1 -1
  104. package/src/assets/web-panel/assets/{slide-eR-f56FQ.js → slide-CSTFi1jq.js} +1 -1
  105. package/src/assets/web-panel/assets/{statusUtils-zcNWczhN.js → statusUtils-DYDvuHx4.js} +1 -1
  106. package/src/assets/web-panel/assets/{styleChecker-u9Z0IfRy.js → styleChecker-BMkbm-8r.js} +1 -1
  107. package/src/assets/web-panel/assets/{transition-gRK4XSlW.js → transition-B3-w1SVL.js} +1 -1
  108. package/src/assets/web-panel/assets/{useConfigInject-ZEunuNHN.js → useConfigInject-DIr4olCu.js} +2 -2
  109. package/src/assets/web-panel/assets/useFlexGapSupport-d0_YPT4c.js +1 -0
  110. package/src/assets/web-panel/assets/{useMergedState-CXfbNKuO.js → useMergedState-DUMpRiCy.js} +1 -1
  111. package/src/assets/web-panel/assets/{useRefs-DwsdQTxa.js → useRefs-C8A7zAB_.js} +1 -1
  112. package/src/assets/web-panel/assets/{useState-DRbnp348.js → useState-CSbzOa8O.js} +1 -1
  113. package/src/assets/web-panel/assets/{vendor-C5RM7MZO.js → vendor-aH7YaPZi.js} +1 -1
  114. package/src/assets/web-panel/assets/{vnode-DVHvXn9F.js → vnode-Df_mZ5AJ.js} +1 -1
  115. package/src/assets/web-panel/assets/{ws-D-sl0vsW.js → ws-BTS8ehTm.js} +1 -1
  116. package/src/assets/web-panel/assets/{zoom-ByzgJIn6.js → zoom-CY5xo2Ia.js} +1 -1
  117. package/src/assets/web-panel/index.html +3 -3
  118. package/src/commands/mtc.js +786 -0
  119. package/src/gateways/ws/ws-server.js +20 -2
  120. package/src/index.js +3 -0
  121. package/src/lib/packer/pkg-runner.js +59 -5
  122. package/src/assets/web-panel/assets/AppLayout-3qsE1-pz.js +0 -1
  123. package/src/assets/web-panel/assets/AppLayout-nff99EgA.css +0 -1
  124. package/src/assets/web-panel/assets/Dashboard-A1TZC5_t.js +0 -3
  125. package/src/assets/web-panel/assets/Portal-CFB5Y97t.js +0 -1
  126. package/src/assets/web-panel/assets/Trigger-xAvohiq9.js +0 -1
  127. package/src/assets/web-panel/assets/icons-CpgFsfkd.js +0 -57
  128. package/src/assets/web-panel/assets/index-8qWxPHSb.js +0 -1
  129. package/src/assets/web-panel/assets/index-BJx6C3J8.js +0 -1
  130. package/src/assets/web-panel/assets/index-BYShDlZ0.js +0 -1
  131. package/src/assets/web-panel/assets/index-CKR2ITFk.js +0 -1
  132. package/src/assets/web-panel/assets/index-D3UDIt7h.js +0 -1
  133. package/src/assets/web-panel/assets/index-D90sLw5Q.js +0 -1
  134. package/src/assets/web-panel/assets/index-Dn_OQQaV.js +0 -36
  135. package/src/assets/web-panel/assets/index-PT376OZM.js +0 -1
  136. package/src/assets/web-panel/assets/index-xL8gcpmy.js +0 -1
  137. package/src/assets/web-panel/assets/useFlexGapSupport-BpbEJfeh.js +0 -1
@@ -0,0 +1,786 @@
1
+ /**
2
+ * Merkle Tree Certificate (MTC) commands
3
+ * chainlesschain mtc batch | verify | landmark inspect
4
+ *
5
+ * Phase 1 Week 3+4 — file-IO only; libp2p / IPFS distribution is future work.
6
+ * Tree-head signature uses real Ed25519 (via @noble/curves) as a stop-gap;
7
+ * SLH-DSA-128f remains the protocol target — module will be swapped when
8
+ * @noble/post-quantum lands without touching CLI / cache.
9
+ */
10
+
11
+ import fs from "node:fs";
12
+ import path from "node:path";
13
+ import chalk from "chalk";
14
+ import { logger } from "../lib/logger.js";
15
+ import { bootstrap, shutdown } from "../runtime/bootstrap.js";
16
+ import { getAllIdentities, getIdentity } from "../lib/did-manager.js";
17
+ import { CLISkillLoader } from "../lib/skill-loader.js";
18
+ import mtcLib from "@chainlesschain/core-mtc";
19
+
20
+ const {
21
+ MerkleTree,
22
+ encodeHashStr,
23
+ sha256,
24
+ leafHash,
25
+ jcs,
26
+ LandmarkCache,
27
+ verify,
28
+ SCHEMA_ENVELOPE,
29
+ SCHEMA_TREE_HEAD,
30
+ SCHEMA_LANDMARK,
31
+ TREE_HEAD_SIG_PREFIX,
32
+ ed25519,
33
+ } = mtcLib;
34
+
35
+ const STOPGAP_BANNER = chalk.yellow(
36
+ "⚠ STOPGAP — tree-head signed with Ed25519 (will switch to SLH-DSA when @noble/post-quantum lands).",
37
+ );
38
+
39
+ function readJsonFile(filePath) {
40
+ const raw = fs.readFileSync(filePath, "utf-8");
41
+ try {
42
+ return JSON.parse(raw);
43
+ } catch (err) {
44
+ throw new Error(`Failed to parse JSON from ${filePath}: ${err.message}`);
45
+ }
46
+ }
47
+
48
+ function writeJsonFile(filePath, obj) {
49
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
50
+ fs.writeFileSync(filePath, JSON.stringify(obj, null, 2), "utf-8");
51
+ }
52
+
53
+ function loadOrGenerateKeyPair(secretKeyPath) {
54
+ if (secretKeyPath && fs.existsSync(secretKeyPath)) {
55
+ const secretKey = Buffer.from(
56
+ fs.readFileSync(secretKeyPath, "utf-8").trim(),
57
+ "hex",
58
+ );
59
+ if (secretKey.length !== 32) {
60
+ throw new Error(
61
+ `Secret key file ${secretKeyPath} must contain 32 bytes (64 hex chars)`,
62
+ );
63
+ }
64
+ // Derive publicKey from secretKey via @noble/curves through ed25519.* — use generateKeyPair-style reload
65
+ // Simpler: re-derive via a helper. Since lib doesn't expose pubkey-from-secret directly, sign a probe
66
+ // and use ed25519.signRaw + ed25519.signTreeHead to recover pubkey isn't possible. Fall back to using
67
+ // a fresh keypair if no helper. Rather than add a helper, use @noble/curves directly here.
68
+ const { ed25519: ed25519Curve } = require("@noble/curves/ed25519");
69
+ const publicKey = Buffer.from(ed25519Curve.getPublicKey(secretKey));
70
+ return {
71
+ secretKey,
72
+ publicKey,
73
+ pubkeyId: ed25519.pubkeyId(publicKey),
74
+ };
75
+ }
76
+ return ed25519.generateKeyPair();
77
+ }
78
+
79
+ function buildBatch(rawLeaves, opts) {
80
+ const namespace = opts.namespace;
81
+ const issuer = opts.issuer;
82
+ const issuedAt = opts.issuedAt || new Date().toISOString();
83
+ const expiresAt =
84
+ opts.expiresAt || new Date(Date.now() + 7 * 24 * 3600 * 1000).toISOString();
85
+
86
+ const keys = loadOrGenerateKeyPair(opts.secretKeyFile);
87
+
88
+ const leafHashes = rawLeaves.map((l) => leafHash(jcs(l)));
89
+ const tree = new MerkleTree(leafHashes);
90
+ const root = tree.root();
91
+
92
+ const treeHead = {
93
+ schema: SCHEMA_TREE_HEAD,
94
+ namespace,
95
+ tree_size: leafHashes.length,
96
+ root_hash: encodeHashStr(root),
97
+ issued_at: issuedAt,
98
+ expires_at: expiresAt,
99
+ issuer,
100
+ };
101
+ const canonical = jcs(treeHead);
102
+ const treeHeadId = encodeHashStr(sha256(canonical));
103
+ const signingInput = Buffer.concat([TREE_HEAD_SIG_PREFIX, canonical]);
104
+ const signature = ed25519.signTreeHead(signingInput, {
105
+ secretKey: keys.secretKey,
106
+ publicKey: keys.publicKey,
107
+ issuer,
108
+ });
109
+
110
+ const landmark = {
111
+ schema: SCHEMA_LANDMARK,
112
+ namespace: namespace.split("/").slice(0, -1).join("/"),
113
+ snapshots: [{ tree_head: treeHead, tree_head_id: treeHeadId, signature }],
114
+ trust_anchors: [ed25519.trustAnchorEntry(keys.publicKey, issuer)],
115
+ published_at: issuedAt,
116
+ publisher_signature: {
117
+ alg: "Ed25519",
118
+ key_id: issuer + "#key-1",
119
+ sig: "TODO-PUBLISHER-SIG",
120
+ },
121
+ };
122
+
123
+ const envelopes = rawLeaves.map((leaf, i) => ({
124
+ schema: SCHEMA_ENVELOPE,
125
+ namespace,
126
+ tree_head_id: treeHeadId,
127
+ leaf,
128
+ inclusion_proof: {
129
+ leaf_index: i,
130
+ tree_size: leafHashes.length,
131
+ audit_path: tree.prove(i).map((b) => encodeHashStr(b)),
132
+ },
133
+ }));
134
+
135
+ return { landmark, envelopes, treeHeadId, keys };
136
+ }
137
+
138
+ export function registerMtcCommand(program) {
139
+ const mtc = program
140
+ .command("mtc")
141
+ .description("Merkle Tree Certificates — batch issuance & verification");
142
+
143
+ // mtc batch
144
+ mtc
145
+ .command("batch")
146
+ .description("Build a tree + landmark + envelopes from a list of leaves")
147
+ .argument("<input>", "JSON file containing an array of leaf objects")
148
+ .requiredOption("--namespace <ns>", "Namespace, e.g. mtc/v1/did/000142")
149
+ .requiredOption(
150
+ "--issuer <issuer>",
151
+ "MTCA issuer string, e.g. mtca:cc:zQ3sh...",
152
+ )
153
+ .option("--out <dir>", "Output directory (default: ./mtc-out)", "./mtc-out")
154
+ .option("--issued-at <iso>", "Override issued_at timestamp")
155
+ .option("--expires-at <iso>", "Override expires_at timestamp")
156
+ .option(
157
+ "--secret-key-file <path>",
158
+ "Reuse Ed25519 secret key from this hex file (creates one if missing)",
159
+ )
160
+ .option("--json", "Print JSON summary instead of human output")
161
+ .action(async (inputPath, options) => {
162
+ try {
163
+ const rawLeaves = readJsonFile(inputPath);
164
+ if (!Array.isArray(rawLeaves) || rawLeaves.length === 0) {
165
+ throw new Error("Input must be a non-empty JSON array of leaves");
166
+ }
167
+
168
+ const { landmark, envelopes, treeHeadId, keys } = buildBatch(
169
+ rawLeaves,
170
+ {
171
+ namespace: options.namespace,
172
+ issuer: options.issuer,
173
+ issuedAt: options.issuedAt,
174
+ expiresAt: options.expiresAt,
175
+ secretKeyFile: options.secretKeyFile,
176
+ },
177
+ );
178
+
179
+ const outDir = path.resolve(options.out);
180
+ const landmarkPath = path.join(outDir, "landmark.json");
181
+ writeJsonFile(landmarkPath, landmark);
182
+
183
+ // If a secret-key-file path was given but didn't exist, save the new key for reuse
184
+ if (options.secretKeyFile && !fs.existsSync(options.secretKeyFile)) {
185
+ fs.mkdirSync(path.dirname(options.secretKeyFile), {
186
+ recursive: true,
187
+ });
188
+ fs.writeFileSync(
189
+ options.secretKeyFile,
190
+ keys.secretKey.toString("hex"),
191
+ { mode: 0o600 },
192
+ );
193
+ }
194
+
195
+ const envelopePaths = [];
196
+ for (let i = 0; i < envelopes.length; i++) {
197
+ const p = path.join(
198
+ outDir,
199
+ `envelope-${String(i).padStart(6, "0")}.json`,
200
+ );
201
+ writeJsonFile(p, envelopes[i]);
202
+ envelopePaths.push(p);
203
+ }
204
+
205
+ if (options.json) {
206
+ console.log(
207
+ JSON.stringify(
208
+ {
209
+ ok: true,
210
+ tree_head_id: treeHeadId,
211
+ tree_size: landmark.snapshots[0].tree_head.tree_size,
212
+ root_hash: landmark.snapshots[0].tree_head.root_hash,
213
+ landmark_path: landmarkPath,
214
+ envelope_paths: envelopePaths,
215
+ },
216
+ null,
217
+ 2,
218
+ ),
219
+ );
220
+ } else {
221
+ logger.log(STOPGAP_BANNER);
222
+ logger.success("Batch built");
223
+ logger.log(` ${chalk.bold("Namespace:")} ${options.namespace}`);
224
+ logger.log(` ${chalk.bold("Tree size:")} ${rawLeaves.length}`);
225
+ logger.log(
226
+ ` ${chalk.bold("Root hash:")} ${landmark.snapshots[0].tree_head.root_hash}`,
227
+ );
228
+ logger.log(` ${chalk.bold("Tree head ID:")} ${treeHeadId}`);
229
+ logger.log(
230
+ ` ${chalk.bold("Landmark:")} ${chalk.cyan(landmarkPath)}`,
231
+ );
232
+ logger.log(
233
+ ` ${chalk.bold("Envelopes:")} ${envelopePaths.length} files in ${outDir}`,
234
+ );
235
+ }
236
+ } catch (err) {
237
+ logger.error(`mtc batch failed: ${err.message}`);
238
+ process.exit(1);
239
+ }
240
+ });
241
+
242
+ // mtc verify
243
+ mtc
244
+ .command("verify")
245
+ .description("Verify an MTC envelope against a landmark")
246
+ .argument("<envelope>", "Path to envelope JSON file")
247
+ .requiredOption("--landmark <path>", "Path to landmark JSON file")
248
+ .option("--now <iso>", "Override current time for expiry check (ISO 8601)")
249
+ .option("--json", "Output structured JSON")
250
+ .action(async (envelopePath, options) => {
251
+ try {
252
+ const envelope = readJsonFile(envelopePath);
253
+ const landmark = readJsonFile(options.landmark);
254
+
255
+ const cache = new LandmarkCache({
256
+ signatureVerifier: ed25519.makeVerifierFromLandmark(landmark),
257
+ });
258
+ cache.ingest(landmark);
259
+
260
+ const now = options.now ? Date.parse(options.now) : Date.now();
261
+ const result = verify(envelope, cache, { now });
262
+
263
+ if (options.json) {
264
+ console.log(JSON.stringify(result, null, 2));
265
+ if (!result.ok) process.exit(2);
266
+ return;
267
+ }
268
+ if (result.ok) {
269
+ logger.log(STOPGAP_BANNER);
270
+ logger.success(`Envelope verified`);
271
+ logger.log(
272
+ ` ${chalk.bold("Subject:")} ${result.leaf.subject || "(no subject)"}`,
273
+ );
274
+ logger.log(
275
+ ` ${chalk.bold("Kind:")} ${result.leaf.kind || "(unknown)"}`,
276
+ );
277
+ logger.log(
278
+ ` ${chalk.bold("Tree size:")} ${result.treeHead.tree_size}`,
279
+ );
280
+ logger.log(` ${chalk.bold("Issuer:")} ${result.treeHead.issuer}`);
281
+ } else {
282
+ logger.error(`Verification failed: ${result.code}`);
283
+ if (result.recoverable) {
284
+ logger.log(
285
+ chalk.yellow(
286
+ " This error is recoverable — try fetching a fresher landmark.",
287
+ ),
288
+ );
289
+ }
290
+ process.exit(2);
291
+ }
292
+ } catch (err) {
293
+ logger.error(`mtc verify failed: ${err.message}`);
294
+ process.exit(1);
295
+ }
296
+ });
297
+
298
+ // mtc landmark inspect
299
+ const landmarkCmd = mtc.command("landmark").description("Landmark utilities");
300
+
301
+ landmarkCmd
302
+ .command("inspect")
303
+ .description("Pretty-print a landmark file's structure")
304
+ .argument("<landmark>", "Path to landmark JSON file")
305
+ .option("--json", "Output structured JSON")
306
+ .action(async (landmarkPath, options) => {
307
+ try {
308
+ const landmark = readJsonFile(landmarkPath);
309
+ if (landmark.schema !== SCHEMA_LANDMARK) {
310
+ throw new Error(`Not a landmark file (schema = ${landmark.schema})`);
311
+ }
312
+
313
+ const summary = {
314
+ namespace_prefix: landmark.namespace,
315
+ published_at: landmark.published_at,
316
+ snapshot_count: (landmark.snapshots || []).length,
317
+ trust_anchor_count: (landmark.trust_anchors || []).length,
318
+ snapshots: (landmark.snapshots || []).map((s) => ({
319
+ namespace: s.tree_head.namespace,
320
+ tree_size: s.tree_head.tree_size,
321
+ root_hash: s.tree_head.root_hash,
322
+ tree_head_id: s.tree_head_id,
323
+ issued_at: s.tree_head.issued_at,
324
+ expires_at: s.tree_head.expires_at,
325
+ issuer: s.tree_head.issuer,
326
+ signature_alg: s.signature && s.signature.alg,
327
+ })),
328
+ };
329
+
330
+ if (options.json) {
331
+ console.log(JSON.stringify(summary, null, 2));
332
+ } else {
333
+ logger.log(`${chalk.bold("Landmark:")} ${landmarkPath}`);
334
+ logger.log(
335
+ `${chalk.bold("Namespace prefix:")} ${summary.namespace_prefix}`,
336
+ );
337
+ logger.log(
338
+ `${chalk.bold("Published at:")} ${summary.published_at}`,
339
+ );
340
+ logger.log(
341
+ `${chalk.bold("Snapshots:")} ${summary.snapshot_count}`,
342
+ );
343
+ logger.log(
344
+ `${chalk.bold("Trust anchors:")} ${summary.trust_anchor_count}`,
345
+ );
346
+ for (const s of summary.snapshots) {
347
+ logger.log("");
348
+ logger.log(` ${chalk.cyan(s.namespace)}`);
349
+ logger.log(` tree_size: ${s.tree_size}`);
350
+ logger.log(` root_hash: ${s.root_hash}`);
351
+ logger.log(` issuer: ${s.issuer}`);
352
+ logger.log(` expires_at: ${s.expires_at}`);
353
+ logger.log(` sig_alg: ${s.signature_alg}`);
354
+ }
355
+ }
356
+ } catch (err) {
357
+ logger.error(`mtc landmark inspect failed: ${err.message}`);
358
+ process.exit(1);
359
+ }
360
+ });
361
+
362
+ // mtc batch-dids — read DIDs from local DB and produce a batch
363
+ mtc
364
+ .command("batch-dids")
365
+ .description("Build a batch from DIDs in the local DB (all by default)")
366
+ .requiredOption("--namespace <ns>", "Namespace, e.g. mtc/v1/did/000007")
367
+ .requiredOption("--issuer <issuer>", "MTCA issuer string")
368
+ .option(
369
+ "--did <did>",
370
+ "Include only this DID (repeatable)",
371
+ (val, prev) => [...(prev || []), val],
372
+ )
373
+ .option("--out <dir>", "Output directory (default: ./mtc-out)", "./mtc-out")
374
+ .option("--issued-at <iso>", "Override issued_at timestamp")
375
+ .option("--expires-at <iso>", "Override expires_at timestamp")
376
+ .option(
377
+ "--secret-key-file <path>",
378
+ "Reuse Ed25519 secret key from this hex file (creates one if missing)",
379
+ )
380
+ .option("--json", "Print JSON summary instead of human output")
381
+ .action(async (options) => {
382
+ let ctx;
383
+ try {
384
+ ctx = await bootstrap({});
385
+ if (!ctx.db) throw new Error("Database not available");
386
+ const db = ctx.db.getDatabase();
387
+
388
+ let rows;
389
+ if (options.did && options.did.length > 0) {
390
+ rows = options.did
391
+ .map((d) => getIdentity(db, d))
392
+ .filter((r) => r != null);
393
+ if (rows.length === 0)
394
+ throw new Error("No matching DIDs found in local DB");
395
+ } else {
396
+ rows = getAllIdentities(db);
397
+ if (rows.length === 0) {
398
+ throw new Error(
399
+ "No DIDs in local DB. Run `cc did create` first or pass --did <did>.",
400
+ );
401
+ }
402
+ }
403
+
404
+ const rawLeaves = rows.map((row) => {
405
+ let didDoc;
406
+ try {
407
+ didDoc = JSON.parse(row.did_document);
408
+ } catch (_err) {
409
+ throw new Error(`DID ${row.did} has malformed did_document in DB`);
410
+ }
411
+ return {
412
+ kind: "did-document",
413
+ content_hash: encodeHashStr(sha256(jcs(didDoc))),
414
+ issued_at: row.created_at || new Date().toISOString(),
415
+ subject: row.did,
416
+ metadata: { version: "1.0.0", supersedes: null },
417
+ };
418
+ });
419
+
420
+ const { landmark, envelopes, treeHeadId, keys } = buildBatch(
421
+ rawLeaves,
422
+ {
423
+ namespace: options.namespace,
424
+ issuer: options.issuer,
425
+ issuedAt: options.issuedAt,
426
+ expiresAt: options.expiresAt,
427
+ secretKeyFile: options.secretKeyFile,
428
+ },
429
+ );
430
+
431
+ const outDir = path.resolve(options.out);
432
+ const landmarkPath = path.join(outDir, "landmark.json");
433
+ writeJsonFile(landmarkPath, landmark);
434
+
435
+ if (options.secretKeyFile && !fs.existsSync(options.secretKeyFile)) {
436
+ fs.mkdirSync(path.dirname(options.secretKeyFile), {
437
+ recursive: true,
438
+ });
439
+ fs.writeFileSync(
440
+ options.secretKeyFile,
441
+ keys.secretKey.toString("hex"),
442
+ { mode: 0o600 },
443
+ );
444
+ }
445
+
446
+ const envelopePaths = [];
447
+ for (let i = 0; i < envelopes.length; i++) {
448
+ const p = path.join(
449
+ outDir,
450
+ `envelope-${String(i).padStart(6, "0")}.json`,
451
+ );
452
+ writeJsonFile(p, envelopes[i]);
453
+ envelopePaths.push(p);
454
+ }
455
+
456
+ const subjects = rawLeaves.map((l) => l.subject);
457
+
458
+ if (options.json) {
459
+ console.log(
460
+ JSON.stringify(
461
+ {
462
+ ok: true,
463
+ tree_head_id: treeHeadId,
464
+ tree_size: rawLeaves.length,
465
+ root_hash: landmark.snapshots[0].tree_head.root_hash,
466
+ landmark_path: landmarkPath,
467
+ envelope_paths: envelopePaths,
468
+ subjects,
469
+ },
470
+ null,
471
+ 2,
472
+ ),
473
+ );
474
+ } else {
475
+ logger.log(STOPGAP_BANNER);
476
+ logger.success(`Batched ${rawLeaves.length} DID(s)`);
477
+ logger.log(` ${chalk.bold("Namespace:")} ${options.namespace}`);
478
+ logger.log(` ${chalk.bold("Tree size:")} ${rawLeaves.length}`);
479
+ logger.log(
480
+ ` ${chalk.bold("Root hash:")} ${landmark.snapshots[0].tree_head.root_hash}`,
481
+ );
482
+ logger.log(` ${chalk.bold("Tree head ID:")} ${treeHeadId}`);
483
+ logger.log(
484
+ ` ${chalk.bold("Landmark:")} ${chalk.cyan(landmarkPath)}`,
485
+ );
486
+ logger.log(
487
+ ` ${chalk.bold("Envelopes:")} ${envelopePaths.length} files in ${outDir}`,
488
+ );
489
+ logger.log(` ${chalk.bold("Subjects:")}`);
490
+ for (const s of subjects) logger.log(` ${chalk.gray(s)}`);
491
+ }
492
+
493
+ await shutdown();
494
+ } catch (err) {
495
+ logger.error(`mtc batch-dids failed: ${err.message}`);
496
+ if (ctx) await shutdown().catch(() => {});
497
+ process.exit(1);
498
+ }
499
+ });
500
+
501
+ // mtc batch-skills — read local skills and produce a batch
502
+ mtc
503
+ .command("batch-skills")
504
+ .description("Build a batch from local CLI skills (all by default)")
505
+ .requiredOption("--namespace <ns>", "Namespace, e.g. mtc/v1/skill/000001")
506
+ .requiredOption("--issuer <issuer>", "MTCA issuer string")
507
+ .option(
508
+ "--skill <id>",
509
+ "Include only this skill id (repeatable)",
510
+ (v, prev) => [...(prev || []), v],
511
+ )
512
+ .option("--out <dir>", "Output directory (default: ./mtc-out)", "./mtc-out")
513
+ .option("--issued-at <iso>", "Override issued_at timestamp")
514
+ .option("--expires-at <iso>", "Override expires_at timestamp")
515
+ .option(
516
+ "--secret-key-file <path>",
517
+ "Reuse Ed25519 secret key from this hex file (creates one if missing)",
518
+ )
519
+ .option("--json", "Print JSON summary instead of human output")
520
+ .action(async (options) => {
521
+ try {
522
+ const loader = new CLISkillLoader();
523
+ const allSkills = loader.loadAll();
524
+ if (allSkills.length === 0) {
525
+ throw new Error(
526
+ "No skills found in any layer. Set up skill packs first.",
527
+ );
528
+ }
529
+
530
+ let skills;
531
+ if (options.skill && options.skill.length > 0) {
532
+ const want = new Set(options.skill);
533
+ skills = allSkills.filter((s) => want.has(s.id));
534
+ if (skills.length === 0) {
535
+ throw new Error("No skills matched --skill filters");
536
+ }
537
+ } else {
538
+ skills = allSkills;
539
+ }
540
+
541
+ const rawLeaves = skills.map((s) => {
542
+ // Hash a canonical view of the skill (id+version+category+description+body fingerprint)
543
+ const canonicalSkill = {
544
+ id: s.id,
545
+ displayName: s.displayName,
546
+ description: s.description,
547
+ version: s.version,
548
+ category: s.category,
549
+ activation: s.activation,
550
+ tags: s.tags,
551
+ };
552
+ return {
553
+ kind: "skill-manifest",
554
+ content_hash: encodeHashStr(sha256(jcs(canonicalSkill))),
555
+ issued_at: new Date().toISOString(),
556
+ subject: `skill:cc:${s.id}@${s.version}`,
557
+ metadata: {
558
+ publisher: options.issuer,
559
+ skill_id: s.id,
560
+ version: s.version,
561
+ category: s.category,
562
+ },
563
+ };
564
+ });
565
+
566
+ const { landmark, envelopes, treeHeadId, keys } = buildBatch(
567
+ rawLeaves,
568
+ {
569
+ namespace: options.namespace,
570
+ issuer: options.issuer,
571
+ issuedAt: options.issuedAt,
572
+ expiresAt: options.expiresAt,
573
+ secretKeyFile: options.secretKeyFile,
574
+ },
575
+ );
576
+
577
+ const outDir = path.resolve(options.out);
578
+ const landmarkPath = path.join(outDir, "landmark.json");
579
+ writeJsonFile(landmarkPath, landmark);
580
+
581
+ if (options.secretKeyFile && !fs.existsSync(options.secretKeyFile)) {
582
+ fs.mkdirSync(path.dirname(options.secretKeyFile), {
583
+ recursive: true,
584
+ });
585
+ fs.writeFileSync(
586
+ options.secretKeyFile,
587
+ keys.secretKey.toString("hex"),
588
+ { mode: 0o600 },
589
+ );
590
+ }
591
+
592
+ const envelopePaths = [];
593
+ for (let i = 0; i < envelopes.length; i++) {
594
+ const p = path.join(
595
+ outDir,
596
+ `envelope-${String(i).padStart(6, "0")}.json`,
597
+ );
598
+ writeJsonFile(p, envelopes[i]);
599
+ envelopePaths.push(p);
600
+ }
601
+
602
+ const subjects = rawLeaves.map((l) => l.subject);
603
+
604
+ if (options.json) {
605
+ console.log(
606
+ JSON.stringify(
607
+ {
608
+ ok: true,
609
+ tree_head_id: treeHeadId,
610
+ tree_size: rawLeaves.length,
611
+ root_hash: landmark.snapshots[0].tree_head.root_hash,
612
+ landmark_path: landmarkPath,
613
+ envelope_paths: envelopePaths,
614
+ subjects,
615
+ },
616
+ null,
617
+ 2,
618
+ ),
619
+ );
620
+ } else {
621
+ logger.log(STOPGAP_BANNER);
622
+ logger.success(`Batched ${rawLeaves.length} skill(s)`);
623
+ logger.log(` ${chalk.bold("Namespace:")} ${options.namespace}`);
624
+ logger.log(` ${chalk.bold("Tree size:")} ${rawLeaves.length}`);
625
+ logger.log(
626
+ ` ${chalk.bold("Root hash:")} ${landmark.snapshots[0].tree_head.root_hash}`,
627
+ );
628
+ logger.log(` ${chalk.bold("Tree head ID:")} ${treeHeadId}`);
629
+ logger.log(
630
+ ` ${chalk.bold("Landmark:")} ${chalk.cyan(landmarkPath)}`,
631
+ );
632
+ logger.log(
633
+ ` ${chalk.bold("Envelopes:")} ${envelopePaths.length} files in ${outDir}`,
634
+ );
635
+ logger.log(` ${chalk.bold("Subjects:")}`);
636
+ for (const s of subjects) logger.log(` ${chalk.gray(s)}`);
637
+ }
638
+ } catch (err) {
639
+ logger.error(`mtc batch-skills failed: ${err.message}`);
640
+ process.exit(1);
641
+ }
642
+ });
643
+
644
+ // mtc serve — verifier daemon: subscribe to a transport, persist + verify
645
+ mtc
646
+ .command("serve")
647
+ .description(
648
+ "Run a verifier daemon: subscribe to landmarks, persist to disk, ingest",
649
+ )
650
+ .option(
651
+ "--transport <kind>",
652
+ "libp2p | filesystem (default: libp2p)",
653
+ "libp2p",
654
+ )
655
+ .option(
656
+ "--listen <multiaddr>",
657
+ "[libp2p] listen address (default /ip4/127.0.0.1/tcp/0)",
658
+ )
659
+ .option(
660
+ "--connect <multiaddr>",
661
+ "[libp2p] dial this peer on startup (repeatable)",
662
+ (v, prev) => [...(prev || []), v],
663
+ )
664
+ .option(
665
+ "--mode <kind>",
666
+ "[libp2p] direct | gossipsub (default: direct)",
667
+ "direct",
668
+ )
669
+ .option("--drop-zone <dir>", "[filesystem] shared directory to watch")
670
+ .option(
671
+ "--prefix <ns>",
672
+ "namespace prefix to subscribe to (repeatable, default: mtc/v1/did)",
673
+ (v, prev) => [...(prev || []), v],
674
+ )
675
+ .option(
676
+ "--cache-dir <dir>",
677
+ "LandmarkCache persistDir for ingested snapshots",
678
+ )
679
+ .option(
680
+ "--exit-after <n>",
681
+ "exit after receiving N landmarks (test/CI use)",
682
+ (v) => parseInt(v, 10),
683
+ )
684
+ .action(async (options) => {
685
+ let transport;
686
+ try {
687
+ const prefixes =
688
+ options.prefix && options.prefix.length > 0
689
+ ? options.prefix
690
+ : ["mtc/v1/did"];
691
+
692
+ // Build transport
693
+ const kind = options.transport;
694
+ if (kind === "libp2p") {
695
+ const { Libp2pTransport } =
696
+ await import("@chainlesschain/core-mtc/transports/libp2p");
697
+ transport = await Libp2pTransport.create({
698
+ listen: options.listen,
699
+ mode: options.mode,
700
+ });
701
+ for (const peer of options.connect || []) {
702
+ await transport
703
+ .connect(peer)
704
+ .catch((err) =>
705
+ logger.warn(`connect to ${peer} failed: ${err.message}`),
706
+ );
707
+ }
708
+ logger.success(
709
+ `libp2p node listening on:\n ${transport.multiaddrs().join("\n ")}`,
710
+ );
711
+ } else if (kind === "filesystem") {
712
+ if (!options.dropZone) {
713
+ throw new Error(
714
+ "--drop-zone is required when --transport=filesystem",
715
+ );
716
+ }
717
+ const { FilesystemTransport } =
718
+ await import("@chainlesschain/core-mtc/transports/filesystem");
719
+ transport = new FilesystemTransport({ dropZone: options.dropZone });
720
+ logger.success(`filesystem transport watching ${options.dropZone}`);
721
+ } else {
722
+ throw new Error(`Unknown --transport: ${kind}`);
723
+ }
724
+
725
+ // Build cache (no signature verifier yet — fed when first landmark arrives)
726
+ let cache = null;
727
+ let received = 0;
728
+
729
+ for (const prefix of prefixes) {
730
+ transport.subscribe(prefix, async (ann) => {
731
+ try {
732
+ const landmark = await transport.fetch(ann.cid);
733
+
734
+ // Lazy-init cache from first landmark's trust_anchors
735
+ if (!cache) {
736
+ cache = new LandmarkCache({
737
+ signatureVerifier: ed25519.makeVerifierFromLandmark(landmark),
738
+ persistDir: options.cacheDir,
739
+ });
740
+ if (options.cacheDir) {
741
+ const r = cache.loadFromDisk();
742
+ logger.log(
743
+ `cache: loaded ${r.loaded} prior snapshots, ${r.failed.length} failed`,
744
+ );
745
+ }
746
+ }
747
+
748
+ cache.ingest(landmark);
749
+ received++;
750
+ logger.success(
751
+ `[${received}] ${ann.namespace} tree_size=${ann.tree_size} cid=${ann.cid}`,
752
+ );
753
+
754
+ if (options.exitAfter && received >= options.exitAfter) {
755
+ logger.log(
756
+ `reached --exit-after ${options.exitAfter}, shutting down`,
757
+ );
758
+ if (transport.close) await transport.close();
759
+ process.exit(0);
760
+ }
761
+ } catch (err) {
762
+ logger.error(`ingest failed: ${err.message}`);
763
+ }
764
+ });
765
+ logger.log(`subscribed: ${prefix}`);
766
+ }
767
+
768
+ // Keep running. Ctrl+C cleanup.
769
+ const cleanup = async () => {
770
+ logger.log("shutting down…");
771
+ if (transport && transport.close) await transport.close();
772
+ process.exit(0);
773
+ };
774
+ process.once("SIGINT", cleanup);
775
+ process.once("SIGTERM", cleanup);
776
+
777
+ // Daemon: never resolve unless --exit-after triggers
778
+ await new Promise(() => {});
779
+ } catch (err) {
780
+ logger.error(`mtc serve failed: ${err.message}`);
781
+ if (transport && transport.close)
782
+ await transport.close().catch(() => {});
783
+ process.exit(1);
784
+ }
785
+ });
786
+ }