chainlesschain 0.161.12 → 0.162.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 (145) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/.build-hash +1 -1
  3. package/src/assets/web-panel/assets/{AIOps-B1dwBvzW.js → AIOps-5A54O3wF.js} +1 -1
  4. package/src/assets/web-panel/assets/{ActionButton-DfR4oLvh.js → ActionButton-epuY2GkZ.js} +1 -1
  5. package/src/assets/web-panel/assets/{Analytics-D3ZYGXjr.js → Analytics-CIdxw7T5.js} +1 -1
  6. package/src/assets/web-panel/assets/{AppLayout-CsmOoh-7.js → AppLayout-xR6YUHsS.js} +2 -2
  7. package/src/assets/web-panel/assets/{Audit-B4gwDm63.js → Audit-D95nRcdZ.js} +1 -1
  8. package/src/assets/web-panel/assets/{Backup-T42uSArV.js → Backup-Cmw7ZTJD.js} +1 -1
  9. package/src/assets/web-panel/assets/{BaseInput-CFi52nMs.js → BaseInput-DgCEAL_E.js} +1 -1
  10. package/src/assets/web-panel/assets/{Chat-D7Vvok1V.js → Chat-DAQAzZsA.js} +1 -1
  11. package/src/assets/web-panel/assets/{Checkbox-Dwaflpww.js → Checkbox-GRxUANnE.js} +1 -1
  12. package/src/assets/web-panel/assets/{Codegen-BIqx3G0b.js → Codegen--6KIDt1W.js} +1 -1
  13. package/src/assets/web-panel/assets/{Col-DzIUYUNu.js → Col-3s6dxuMx.js} +1 -1
  14. package/src/assets/web-panel/assets/{Community-DKePtzhk.js → Community-D60pkEBz.js} +1 -1
  15. package/src/assets/web-panel/assets/{Compact-CirVV9Wq.js → Compact-CQR4QsWP.js} +1 -1
  16. package/src/assets/web-panel/assets/{Compliance-CrXOr0sy.js → Compliance-KySNEhMK.js} +1 -1
  17. package/src/assets/web-panel/assets/{Cowork-BEBP6Tht.js → Cowork-Yrrf_Vuu.js} +1 -1
  18. package/src/assets/web-panel/assets/{Cron-k7nNUuqh.js → Cron-DSuuSKIQ.js} +1 -1
  19. package/src/assets/web-panel/assets/{Crosschain-CBnX0Dhq.js → Crosschain-Dub0G9i4.js} +1 -1
  20. package/src/assets/web-panel/assets/{DID-B48FszWS.js → DID-CbpCGV51.js} +1 -1
  21. package/src/assets/web-panel/assets/{Dashboard-Pb3qfFpp.js → Dashboard-D29e0y01.js} +2 -2
  22. package/src/assets/web-panel/assets/{Dropdown-pUHy4CQ2.js → Dropdown-DZY7X5v2.js} +1 -1
  23. package/src/assets/web-panel/assets/{Federation-CT0Qs-kR.js → Federation-dmrLzzMB.js} +1 -1
  24. package/src/assets/web-panel/assets/{FormItemContext-CbJJp5BR.js → FormItemContext-EopsCGez.js} +1 -1
  25. package/src/assets/web-panel/assets/{Git-B3mGNLQe.js → Git-D3mb4RhK.js} +1 -1
  26. package/src/assets/web-panel/assets/{Governance-BKf4733q.js → Governance-8v034-Nr.js} +1 -1
  27. package/src/assets/web-panel/assets/{Inference-DZjU541G.js → Inference-Ddl-HQwQ.js} +1 -1
  28. package/src/assets/web-panel/assets/{KnowledgeGraph-C8L-7Dd1.js → KnowledgeGraph-DrRruyud.js} +1 -1
  29. package/src/assets/web-panel/assets/{Logs-CwDwOiKv.js → Logs-Bsu6lYQe.js} +1 -1
  30. package/src/assets/web-panel/assets/{Marketplace-C2YWWU0M.js → Marketplace-Iyf2Xc23.js} +1 -1
  31. package/src/assets/web-panel/assets/{McpTools-dIbkOypF.js → McpTools-ljVE1lIN.js} +1 -1
  32. package/src/assets/web-panel/assets/{Memory-7eF8WzcY.js → Memory-Ca48TtDU.js} +1 -1
  33. package/src/assets/web-panel/assets/{MobileBridge-C74GHLbX.js → MobileBridge-DZ3Ar6da.js} +1 -1
  34. package/src/assets/web-panel/assets/{Mtc-CSEDo5Fo.js → Mtc-CRjBnVAT.js} +1 -1
  35. package/src/assets/web-panel/assets/{MtcAudit-DiJXxOrB.js → MtcAudit-CEC2eKez.js} +1 -1
  36. package/src/assets/web-panel/assets/Multisig-BHoQvNeL.js +7 -0
  37. package/src/assets/web-panel/assets/Multisig-kwPDnXnl.css +1 -0
  38. package/src/assets/web-panel/assets/{NLProgramming-DjF-gIUw.js → NLProgramming-9e5hIFvL.js} +1 -1
  39. package/src/assets/web-panel/assets/{Notes-BUE5CvMO.js → Notes-BEhZRadw.js} +1 -1
  40. package/src/assets/web-panel/assets/{NotificationSettings-Dfbrobje.js → NotificationSettings-CzPbT9UE.js} +1 -1
  41. package/src/assets/web-panel/assets/{Organization-C6YvqjQB.js → Organization-CvFg4VAr.js} +1 -1
  42. package/src/assets/web-panel/assets/{Overflow-BvHNhdMR.js → Overflow-Cm7ISeJl.js} +1 -1
  43. package/src/assets/web-panel/assets/{P2P-BO0hQHFS.js → P2P-BWN-wdfH.js} +1 -1
  44. package/src/assets/web-panel/assets/{Permissions-CCPlrJeP.js → Permissions-C4vKkP_t.js} +1 -1
  45. package/src/assets/web-panel/assets/{Pipeline-DTCL3FjJ.js → Pipeline-DQCShDeT.js} +1 -1
  46. package/src/assets/web-panel/assets/{Privacy-08DYgOe_.js → Privacy-Nc7LlSr3.js} +1 -1
  47. package/src/assets/web-panel/assets/{ProjectInit-B7j-Z8sa.js → ProjectInit-DDkqXAED.js} +1 -1
  48. package/src/assets/web-panel/assets/{ProjectSettings-CFqLhV1w.js → ProjectSettings-ia6GQr6A.js} +1 -1
  49. package/src/assets/web-panel/assets/{Projects-BPlpx2UN.js → Projects-CNqXa5XY.js} +1 -1
  50. package/src/assets/web-panel/assets/{Providers-BCBPbVbF.js → Providers-EwarbUb8.js} +1 -1
  51. package/src/assets/web-panel/assets/{QuickAsk-C8Job6zl.js → QuickAsk-BAHQBw0d.js} +1 -1
  52. package/src/assets/web-panel/assets/{Recommend-DOV_Aje-.js → Recommend-Ccgu6elV.js} +1 -1
  53. package/src/assets/web-panel/assets/{Reputation-DOgK_dIK.js → Reputation-ai0gRNpj.js} +1 -1
  54. package/src/assets/web-panel/assets/{Row-8jdU1xYg.js → Row-BWLfAhHC.js} +1 -1
  55. package/src/assets/web-panel/assets/{RssFeed-B289OgNZ.js → RssFeed-CnuRtiQH.js} +1 -1
  56. package/src/assets/web-panel/assets/{Search-cr0AndFE.js → Search-Da0CvI_2.js} +1 -1
  57. package/src/assets/web-panel/assets/{Security-w4diykaE.js → Security-DEFeOeUv.js} +1 -1
  58. package/src/assets/web-panel/assets/{Services-dTxAI5cG.js → Services-CsfvCqCH.js} +1 -1
  59. package/src/assets/web-panel/assets/{Skeleton-B3FiUiRo.js → Skeleton-DlRaGj_n.js} +1 -1
  60. package/src/assets/web-panel/assets/{Skills-DgtBTAFC.js → Skills-fjlZrsYq.js} +1 -1
  61. package/src/assets/web-panel/assets/{Sla-B4Y6bvbL.js → Sla-DNSDuEt2.js} +1 -1
  62. package/src/assets/web-panel/assets/{SpeechSettings-C8FEvArc.js → SpeechSettings-Bj0t-JCf.js} +1 -1
  63. package/src/assets/web-panel/assets/{SyncSettings-CHvV5L0m.js → SyncSettings-uO7Gy-BB.js} +1 -1
  64. package/src/assets/web-panel/assets/{Tasks-B4Py5ORS.js → Tasks-BSjsO-m8.js} +1 -1
  65. package/src/assets/web-panel/assets/{Templates-MeTyXM5u.js → Templates-B03SSuXn.js} +1 -1
  66. package/src/assets/web-panel/assets/{Tenant-Doiz7wyQ.js → Tenant-CTCPIBzq.js} +1 -1
  67. package/src/assets/web-panel/assets/{Terminal-DSEAdwWN.js → Terminal-CQZcdArx.js} +1 -1
  68. package/src/assets/web-panel/assets/{Tokens-BBrtaEKt.js → Tokens-CsNJIdNl.js} +1 -1
  69. package/src/assets/web-panel/assets/{Trigger-nPvjho27.js → Trigger-kYPQmm6P.js} +1 -1
  70. package/src/assets/web-panel/assets/{Trust-BFwPyS_6.js → Trust-mt-MiBeI.js} +1 -1
  71. package/src/assets/web-panel/assets/{UkeySign-DFsHoY1J.js → UkeySign-DBi8jgXw.js} +1 -1
  72. package/src/assets/web-panel/assets/{VideoEditing-FkSKqHE9.js → VideoEditing-CINSMEf5.js} +1 -1
  73. package/src/assets/web-panel/assets/{Wallet-AXo-_4-w.js → Wallet-DCVMgW0U.js} +1 -1
  74. package/src/assets/web-panel/assets/{WebAuthn-DoScnQ-4.js → WebAuthn-C9OjbTBs.js} +1 -1
  75. package/src/assets/web-panel/assets/{WorkflowEditor-IEMfIax-.js → WorkflowEditor-LM1HJs-S.js} +1 -1
  76. package/src/assets/web-panel/assets/{chat-pc1ciH6T.js → chat-Dz68Ixdp.js} +1 -1
  77. package/src/assets/web-panel/assets/{colors-CjkIkB0e.js → colors-qf63Eax9.js} +1 -1
  78. package/src/assets/web-panel/assets/{compact-item-CQZnkP5p.js → compact-item-r4YWRFGQ.js} +1 -1
  79. package/src/assets/web-panel/assets/{createContext-B1McGnV-.js → createContext-C8Lwrn-C.js} +1 -1
  80. package/src/assets/web-panel/assets/{hasIn-CbkA6peP.js → hasIn-DsPi2kPP.js} +1 -1
  81. package/src/assets/web-panel/assets/{index-B8r6OBuk.js → index-1xqIvVM5.js} +1 -1
  82. package/src/assets/web-panel/assets/{index-Dn7SHV2e.js → index-5nOMJaLt.js} +1 -1
  83. package/src/assets/web-panel/assets/{index-ChCCGHwz.js → index-B0KxtBNy.js} +1 -1
  84. package/src/assets/web-panel/assets/index-B3blziag.js +1 -0
  85. package/src/assets/web-panel/assets/{index-CGke5qZp.js → index-B8gs4g1v.js} +1 -1
  86. package/src/assets/web-panel/assets/{index-DHpwwlmv.js → index-BFV6aAoX.js} +1 -1
  87. package/src/assets/web-panel/assets/{index-DWq64zmv.js → index-BKEl9Ahm.js} +1 -1
  88. package/src/assets/web-panel/assets/{index-NRWBOo4F.js → index-BfdIkZnT.js} +1 -1
  89. package/src/assets/web-panel/assets/{index-fqI1KnP_.js → index-BjPHuhxG.js} +1 -1
  90. package/src/assets/web-panel/assets/{index-DHtiecyM.js → index-BtCSUUKm.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-DjxVsyn_.js → index-BxNXO95B.js} +1 -1
  92. package/src/assets/web-panel/assets/{index-DGb36exe.js → index-Bz1O2KhE.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-DcSe5y5O.js → index-BzzFMBIM.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-uWvLy_3T.js → index-C2OFaKmx.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-BTyhXFDW.js → index-C3VVwJcn.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-C052wi94.js → index-C8SY0_S8.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-wwQ_ZkWN.js → index-CCCX2ZSH.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-DPzT5L14.js → index-CEbbycgJ.js} +3 -3
  99. package/src/assets/web-panel/assets/{index-CzZ3LxPK.js → index-CINVudo7.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-CKwkP66s.js → index-CKRXnUTD.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-D4rhVRSV.js → index-CSCb-EY9.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-DMjPzhsp.js → index-CY5X4uXO.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-5c4JzwY3.js → index-CqmyLTa_.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-CEENN81t.js → index-Cw48kkkH.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-C0Lj7Yl0.js → index-D0mFFS7Y.js} +1 -1
  106. package/src/assets/web-panel/assets/index-D1XYuPHf.js +1 -0
  107. package/src/assets/web-panel/assets/{index-CKledOCh.js → index-D2BR3WQ-.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-DsChgzu2.js → index-D4XgKgOF.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-C5mpCgak.js → index-DBpVVgRI.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-Bs-romIz.js → index-DGXVjiyF.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-BNLeseG1.js → index-DQSweK5-.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-CJr12ypE.js → index-DXBc_lyR.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-CFCQl1hk.js → index-DycrmZ_r.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-Di5tuS0d.js → index-I55BAmVG.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-CJbqE5Sw.js → index-LQYz2tRO.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-DYAWIXFt.js → index-cTFVNgoS.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-Dzfj71tf.js → index-qLyVEtRM.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-DJWYN-AT.js → index-qcVx0s-M.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-DjM0jsm1.js → index-wVpfPJXE.js} +1 -1
  120. package/src/assets/web-panel/assets/{initDefaultProps-BTloFqyf.js → initDefaultProps-CAgx2aHm.js} +1 -1
  121. package/src/assets/web-panel/assets/{motion-44iMBc1o.js → motion-DFuREFOd.js} +1 -1
  122. package/src/assets/web-panel/assets/{move-CyWQI5eW.js → move-DfcQIXuy.js} +1 -1
  123. package/src/assets/web-panel/assets/{omit-CXfJtuFy.js → omit-BFZV2hmZ.js} +1 -1
  124. package/src/assets/web-panel/assets/{pickAttrs-ANFpSZes.js → pickAttrs-D98WQSur.js} +1 -1
  125. package/src/assets/web-panel/assets/{placementArrow-CigX-url.js → placementArrow-BlP2AN9q.js} +1 -1
  126. package/src/assets/web-panel/assets/{responsiveObserve-Ch48RA0m.js → responsiveObserve-DuI2xGQd.js} +1 -1
  127. package/src/assets/web-panel/assets/{slide-BEz3LORF.js → slide-DSvqah_S.js} +1 -1
  128. package/src/assets/web-panel/assets/{statusUtils-D9EniG8V.js → statusUtils-B-96ZLWF.js} +1 -1
  129. package/src/assets/web-panel/assets/{styleChecker-C1IvuY3L.js → styleChecker-Br0YrCOO.js} +1 -1
  130. package/src/assets/web-panel/assets/{useFlexGapSupport-C92oHeXB.js → useFlexGapSupport-BD4Xr9XU.js} +1 -1
  131. package/src/assets/web-panel/assets/{useFs-CJhvFpgB.js → useFs-Ch5L3BnA.js} +1 -1
  132. package/src/assets/web-panel/assets/{vnode-BPkyunN_.js → vnode-BQi3uWRC.js} +1 -1
  133. package/src/assets/web-panel/assets/{zoom-B6Ipb64r.js → zoom-DaW-jbu7.js} +1 -1
  134. package/src/assets/web-panel/index.html +1 -1
  135. package/src/commands/crosschain.js +403 -1
  136. package/src/commands/pair.js +291 -0
  137. package/src/index.js +2 -0
  138. package/src/lib/cross-chain-mtc.js +275 -6
  139. package/src/lib/cross-chain.js +48 -2
  140. package/src/lib/lan-pairing-preflight.js +425 -0
  141. package/src/lib/lan-pairing-tokens.js +264 -0
  142. package/src/assets/web-panel/assets/Multisig-D-IuEDLa.css +0 -1
  143. package/src/assets/web-panel/assets/Multisig-FZTU5ri6.js +0 -1
  144. package/src/assets/web-panel/assets/index-BXfePRef.js +0 -1
  145. package/src/assets/web-panel/assets/index-Dc4fj_Ys.js +0 -1
@@ -0,0 +1,291 @@
1
+ /**
2
+ * `cc pair` — LAN pairing utilities. v0.1 ships only the `preflight`
3
+ * subcommand for Linux LAN-pairing diagnostics (#21 A.1 PR1).
4
+ *
5
+ * Future:
6
+ * cc pair init — headless mode pairing initiator (PR2)
7
+ * cc pair accept --qr ... — accept a scanned QR from CLI (PR2)
8
+ */
9
+
10
+ import chalk from "chalk";
11
+ import { Command } from "commander";
12
+ import {
13
+ runPreflight,
14
+ firewallCommandTemplate,
15
+ detectFirewallTool,
16
+ STATUS,
17
+ } from "../lib/lan-pairing-preflight.js";
18
+ import {
19
+ addToken,
20
+ listTokens,
21
+ findToken,
22
+ revokeToken,
23
+ STATUS as TOKEN_STATUS,
24
+ } from "../lib/lan-pairing-tokens.js";
25
+
26
+ function statusBadge(s) {
27
+ switch (s) {
28
+ case STATUS.OK:
29
+ return chalk.green("✔ ok");
30
+ case STATUS.WARNING:
31
+ return chalk.yellow("⚠ warn");
32
+ case STATUS.BLOCKER:
33
+ return chalk.red("✖ blocker");
34
+ case STATUS.SKIP:
35
+ return chalk.gray("· skip");
36
+ default:
37
+ return s;
38
+ }
39
+ }
40
+
41
+ function printHumanReadable(report, showFirewall) {
42
+ console.log(chalk.bold("\n cc pair preflight\n"));
43
+ for (const c of report.checks) {
44
+ console.log(
45
+ ` ${statusBadge(c.status).padEnd(20)} ${chalk.bold(c.name.padEnd(22))} ${c.detail}`,
46
+ );
47
+ }
48
+ console.log();
49
+ console.log(
50
+ chalk.bold(
51
+ ` summary: ${chalk.green(report.summary.ok + " ok")}, ` +
52
+ `${chalk.yellow(report.summary.warnings + " warning")}, ` +
53
+ `${chalk.red(report.summary.blockers + " blocker")}`,
54
+ ),
55
+ );
56
+
57
+ // Show firewall commands when there's a blocker on multicast_bind, or
58
+ // when --show-firewall is set.
59
+ const multicastBlocked = report.checks.some(
60
+ (c) => c.name === "multicast_bind" && c.status === STATUS.BLOCKER,
61
+ );
62
+ if (showFirewall || multicastBlocked) {
63
+ const tool = detectFirewallTool();
64
+ const tpl = firewallCommandTemplate(tool);
65
+ if (tpl) {
66
+ console.log();
67
+ console.log(chalk.bold(" Firewall fix (detected tool: " + tool + "):"));
68
+ console.log();
69
+ console.log(
70
+ tpl
71
+ .split("\n")
72
+ .map((l) => " " + l)
73
+ .join("\n"),
74
+ );
75
+ }
76
+ }
77
+ console.log();
78
+ }
79
+
80
+ function tokenStatusBadge(s) {
81
+ switch (s) {
82
+ case TOKEN_STATUS.PENDING:
83
+ return chalk.cyan("pending");
84
+ case TOKEN_STATUS.CONSUMED:
85
+ return chalk.green("consumed");
86
+ case TOKEN_STATUS.REVOKED:
87
+ return chalk.gray("revoked");
88
+ case TOKEN_STATUS.EXPIRED:
89
+ return chalk.yellow("expired");
90
+ default:
91
+ return s;
92
+ }
93
+ }
94
+
95
+ function relTime(ms) {
96
+ const diff = ms - Date.now();
97
+ const absMin = Math.abs(diff) / 60_000;
98
+ if (absMin < 60) {
99
+ return diff > 0
100
+ ? `${Math.round(absMin)} min from now`
101
+ : `${Math.round(absMin)} min ago`;
102
+ }
103
+ const absH = absMin / 60;
104
+ if (absH < 24) {
105
+ return diff > 0
106
+ ? `${Math.round(absH)}h from now`
107
+ : `${Math.round(absH)}h ago`;
108
+ }
109
+ return diff > 0
110
+ ? `${Math.round(absH / 24)}d from now`
111
+ : `${Math.round(absH / 24)}d ago`;
112
+ }
113
+
114
+ export function registerPairCommand(program) {
115
+ const pair = program
116
+ .command("pair")
117
+ .description(
118
+ "LAN pairing utilities (preflight diagnostics + pairing token issuance)",
119
+ );
120
+
121
+ pair
122
+ .command("preflight")
123
+ .description(
124
+ "Diagnose Linux LAN pairing — checks multicast bind, port 5353 holders, firewall hints (#21 A.1)",
125
+ )
126
+ .option("--json", "Machine-readable JSON output")
127
+ .option(
128
+ "--show-firewall",
129
+ "Always print firewall fix commands (default: only on blocker)",
130
+ )
131
+ .option(
132
+ "--multicast-bind-timeout-ms <ms>",
133
+ "Timeout for multicast bind probe",
134
+ (v) => parseInt(v, 10),
135
+ )
136
+ .action(async (options) => {
137
+ const report = await runPreflight({
138
+ multicastBindTimeoutMs: options.multicastBindTimeoutMs,
139
+ });
140
+
141
+ if (options.json) {
142
+ console.log(JSON.stringify(report, null, 2));
143
+ process.exitCode = report.exitCode;
144
+ return;
145
+ }
146
+
147
+ printHumanReadable(report, options.showFirewall);
148
+ process.exitCode = report.exitCode;
149
+ });
150
+
151
+ // ─── pair token (PR2) ─────────────────────────────────────────
152
+
153
+ const token = pair
154
+ .command("token")
155
+ .description(
156
+ "Manage pairing tokens (issue/list/show/revoke). Tokens are compatible with desktop device-pairing-handler QR shape.",
157
+ );
158
+
159
+ token
160
+ .command("generate")
161
+ .description("Issue a new 6-digit pairing token for a given DID")
162
+ .requiredOption("--did <did>", "PC DID (the token will pair this DID)")
163
+ .option("--name <name>", "Friendly device name (default: hostname)")
164
+ .option(
165
+ "--device-id <id>",
166
+ "Override deviceId (default: cli-<hostname>-<ts>)",
167
+ )
168
+ .option("--json", "Machine-readable JSON output (full qrData + metadata)")
169
+ .action((options) => {
170
+ const deviceInfo = (() => {
171
+ const out = {};
172
+ if (options.name) out.name = options.name;
173
+ if (options.deviceId) out.deviceId = options.deviceId;
174
+ return Object.keys(out).length ? out : undefined;
175
+ })();
176
+ const t = addToken({ did: options.did, deviceInfo });
177
+ if (options.json) {
178
+ console.log(JSON.stringify(t, null, 2));
179
+ return;
180
+ }
181
+ console.log();
182
+ console.log(chalk.bold(" ✓ Pairing token issued\n"));
183
+ console.log(" code: " + chalk.bold.cyan(t.code));
184
+ console.log(" did: " + t.qrData.did);
185
+ console.log(" device: " + (t.qrData.deviceInfo?.name || "(none)"));
186
+ console.log(" expires: " + relTime(t.expiresAtMs));
187
+ console.log();
188
+ console.log(
189
+ chalk.gray(" QR data (paste into mobile scanner / Electron handler):"),
190
+ );
191
+ console.log();
192
+ console.log(" " + JSON.stringify(t.qrData));
193
+ console.log();
194
+ });
195
+
196
+ token
197
+ .command("list")
198
+ .description("List pairing tokens")
199
+ .option(
200
+ "--status <s>",
201
+ "Filter by status (pending/consumed/revoked/expired)",
202
+ )
203
+ .option("--did <did>", "Filter by DID")
204
+ .option("--json", "Machine-readable JSON output")
205
+ .action((options) => {
206
+ const tokens = listTokens({ status: options.status, did: options.did });
207
+ if (options.json) {
208
+ console.log(JSON.stringify({ tokens }, null, 2));
209
+ return;
210
+ }
211
+ if (tokens.length === 0) {
212
+ console.log(chalk.gray("\n No pairing tokens.\n"));
213
+ return;
214
+ }
215
+ console.log();
216
+ console.log(chalk.bold(" Pairing tokens:\n"));
217
+ console.log(
218
+ " " +
219
+ chalk.gray("code").padEnd(10) +
220
+ chalk.gray("status").padEnd(14) +
221
+ chalk.gray("did").padEnd(30) +
222
+ chalk.gray("expires"),
223
+ );
224
+ for (const t of tokens) {
225
+ console.log(
226
+ " " +
227
+ chalk.bold(t.code).padEnd(20) +
228
+ tokenStatusBadge(t.status).padEnd(20) +
229
+ String(t.qrData?.did || "?")
230
+ .slice(0, 28)
231
+ .padEnd(30) +
232
+ relTime(t.expiresAtMs),
233
+ );
234
+ }
235
+ console.log();
236
+ });
237
+
238
+ token
239
+ .command("show <code>")
240
+ .description("Show a single pairing token by code")
241
+ .option("--json", "Machine-readable JSON output")
242
+ .action((code, options) => {
243
+ const t = findToken(code);
244
+ if (!t) {
245
+ console.error(chalk.red(`✖ token not found: ${code}`));
246
+ process.exitCode = 2;
247
+ return;
248
+ }
249
+ if (options.json) {
250
+ console.log(JSON.stringify(t, null, 2));
251
+ return;
252
+ }
253
+ console.log();
254
+ console.log(chalk.bold(` Token ${chalk.cyan(t.code)}\n`));
255
+ console.log(" status: " + tokenStatusBadge(t.status));
256
+ console.log(" did: " + t.qrData.did);
257
+ console.log(
258
+ " device: " +
259
+ (t.qrData.deviceInfo?.name || "(none)") +
260
+ " (" +
261
+ (t.qrData.deviceInfo?.platform || "?") +
262
+ ")",
263
+ );
264
+ console.log(" created: " + new Date(t.createdAtMs).toISOString());
265
+ console.log(" expires: " + new Date(t.expiresAtMs).toISOString());
266
+ if (t.consumedAtMs)
267
+ console.log(" consumed: " + new Date(t.consumedAtMs).toISOString());
268
+ if (t.revokedAtMs)
269
+ console.log(" revoked: " + new Date(t.revokedAtMs).toISOString());
270
+ console.log();
271
+ });
272
+
273
+ token
274
+ .command("revoke <code>")
275
+ .description("Revoke a pending pairing token")
276
+ .option("--json", "Machine-readable JSON output")
277
+ .action((code, options) => {
278
+ const r = revokeToken(code);
279
+ if (options.json) {
280
+ console.log(JSON.stringify(r, null, 2));
281
+ if (!r.revoked) process.exitCode = 2;
282
+ return;
283
+ }
284
+ if (r.revoked) {
285
+ console.log(chalk.green(`\n ✓ Token ${code} revoked.\n`));
286
+ } else {
287
+ console.error(chalk.red(`\n ✖ Cannot revoke: ${r.reason}\n`));
288
+ process.exitCode = 2;
289
+ }
290
+ });
291
+ }
package/src/index.js CHANGED
@@ -8,6 +8,7 @@ import { registerServicesCommand } from "./commands/services.js";
8
8
  import { registerConfigCommand } from "./commands/config.js";
9
9
  import { registerUpdateCommand } from "./commands/update.js";
10
10
  import { registerDoctorCommand } from "./commands/doctor.js";
11
+ import { registerPairCommand } from "./commands/pair.js";
11
12
  import { registerDbCommand } from "./commands/db.js";
12
13
  import { registerNoteCommand } from "./commands/note.js";
13
14
  import { registerChatCommand } from "./commands/chat.js";
@@ -393,6 +394,7 @@ export function createProgram(opts = {}) {
393
394
  registerConfigCommand(program);
394
395
  registerUpdateCommand(program);
395
396
  registerDoctorCommand(program);
397
+ registerPairCommand(program);
396
398
 
397
399
  // Headless commands
398
400
  registerDbCommand(program);
@@ -421,6 +421,12 @@ function stagedFileName(op) {
421
421
  * Stage one bridge op for inclusion in the next batch close.
422
422
  * Idempotent: same op written twice -> single staging file.
423
423
  *
424
+ * #21 B.5 Layer 2 PR4 — when `op.multisig_provenance` is present, validate
425
+ * its structure via `verifyMultisigProvenance` before writing. Invalid
426
+ * provenance is rejected (`INVALID_MULTISIG_PROVENANCE`) instead of being
427
+ * silently persisted so downstream `closeBatch` / wrapper attachment
428
+ * cannot pick up malformed data.
429
+ *
424
430
  * @param {string} dir
425
431
  * @param {object} op
426
432
  * @param {{ requireEnabled?: boolean }} [opts]
@@ -434,6 +440,18 @@ export function stageBridgeOp(dir, op, opts = {}) {
434
440
  return { staged: false, path: null, reason: "DISABLED" };
435
441
  }
436
442
  validateBridgeOp(op);
443
+ if (op.multisig_provenance) {
444
+ const pv = verifyMultisigProvenance({
445
+ multisig_provenance: op.multisig_provenance,
446
+ });
447
+ if (!pv.ok) {
448
+ return {
449
+ staged: false,
450
+ path: null,
451
+ reason: `INVALID_MULTISIG_PROVENANCE:${pv.code}${pv.detail ? `:${pv.detail}` : ""}`,
452
+ };
453
+ }
454
+ }
437
455
  const file = path.join(stagingDir(dir), stagedFileName(op));
438
456
  if (fs.existsSync(file)) {
439
457
  return { staged: false, path: file, reason: "ALREADY_STAGED" };
@@ -564,10 +582,203 @@ export function closeBatch(dir, opts = {}) {
564
582
 
565
583
  // ─────────────────────────────────────────────────────────────────────
566
584
  // v0.2 — Multi-hop envelope (envelope-of-envelope)
585
+ // v0.3 — #21 B.5 Layer 2 PR2: m-of-n multisig provenance on the wrapper
567
586
  // ─────────────────────────────────────────────────────────────────────
568
587
 
569
588
  const SCHEMA_MULTI_HOP_BRIDGE = "mtc-bridge-multihop/v1";
570
589
 
590
+ const MULTISIG_PROVENANCE_REQUIRED_FIELDS = Object.freeze([
591
+ "proposal_id",
592
+ "threshold_m",
593
+ "member_count_n",
594
+ "signers",
595
+ "partial_sigs",
596
+ ]);
597
+
598
+ const MULTISIG_SUPPORTED_ALGS = Object.freeze(["Ed25519", "SLH-DSA-128F"]);
599
+
600
+ /**
601
+ * Attach m-of-n multisig provenance to a bridge envelope (single-hop or
602
+ * multi-hop wrapper). Returns a new envelope object — does not mutate.
603
+ *
604
+ * Provenance carries the proposal id, the m/n policy at consume time, the
605
+ * sorted-ASC signer DID list, and per-signer {did, alg, sig:hex} entries.
606
+ * Mirrors the cc_bridges columns introduced in #21 B.5 Layer 2 PR1.
607
+ *
608
+ * Layer 3 onchain broadcasters read this field to construct the m-of-n
609
+ * proof bundle expected by destination-chain verifier contracts.
610
+ *
611
+ * @param {object} envelope - mtc-envelope/v1 or mtc-bridge-multihop/v1
612
+ * @param {{
613
+ * proposalId: string,
614
+ * thresholdM: number,
615
+ * memberCountN: number,
616
+ * signers: string[],
617
+ * partialSigs: Array<{did: string, alg: string, sig: string}>,
618
+ * }} provenance
619
+ * @returns {object} new envelope with `multisig_provenance` field
620
+ */
621
+ export function attachMultisigProvenance(envelope, provenance) {
622
+ if (!envelope || typeof envelope !== "object") {
623
+ throw new TypeError("attachMultisigProvenance: envelope required");
624
+ }
625
+ if (!provenance || typeof provenance !== "object") {
626
+ throw new TypeError("attachMultisigProvenance: provenance required");
627
+ }
628
+ return {
629
+ ...envelope,
630
+ multisig_provenance: {
631
+ proposal_id: provenance.proposalId,
632
+ threshold_m: provenance.thresholdM,
633
+ member_count_n: provenance.memberCountN,
634
+ signers: Array.isArray(provenance.signers) ? [...provenance.signers] : [],
635
+ partial_sigs: Array.isArray(provenance.partialSigs)
636
+ ? provenance.partialSigs.map((s) => ({
637
+ did: s.did,
638
+ alg: s.alg,
639
+ sig: s.sig,
640
+ }))
641
+ : [],
642
+ },
643
+ };
644
+ }
645
+
646
+ /**
647
+ * Strip-all-sigs canonical form for a bridge envelope carrying
648
+ * `multisig_provenance`. Returns a clone with every `partial_sigs[*].sig`
649
+ * replaced by `""` so producer and verifier can feed an identical byte
650
+ * sequence into JCS for m-of-n threshold tolerance.
651
+ *
652
+ * Mirrors `@chainlesschain/core-mtc/lib/publisher-signing.js
653
+ * _stripSigsForPublisher` (memory `mtc_publisher_sig_threshold.md`) — both
654
+ * the publisher_signature on a landmark and a wrapper-level m-of-n proof
655
+ * need the same canonical-skeleton-without-sigs invariant.
656
+ *
657
+ * Reused by PR3 verifier when checking each partial sig against the
658
+ * canonical wrapper bytes.
659
+ *
660
+ * @param {object} envelope - envelope with `multisig_provenance`
661
+ * @returns {object} clone with sigs zeroed; same envelope returned when no
662
+ * provenance is attached
663
+ */
664
+ export function stripMultisigSigsForCanonical(envelope) {
665
+ if (!envelope || !envelope.multisig_provenance) return envelope;
666
+ return {
667
+ ...envelope,
668
+ multisig_provenance: {
669
+ ...envelope.multisig_provenance,
670
+ partial_sigs: (envelope.multisig_provenance.partial_sigs || []).map(
671
+ (s) => ({ ...s, sig: "" }),
672
+ ),
673
+ },
674
+ };
675
+ }
676
+
677
+ /**
678
+ * Structural validation of multisig_provenance on a bridge envelope.
679
+ * Does NOT verify cryptographic signatures (deferred to PR3 — would need
680
+ * the proposal's payload JCS + member pubkey lookup). Confirms:
681
+ * - provenance is present and well-formed
682
+ * - partial_sigs.length >= threshold_m
683
+ * - every signer DID is in the policy.members allow-list (when supplied)
684
+ * - every partial_sig has a supported alg + non-empty hex sig
685
+ * - signer list is sorted ASC (canonical form invariant)
686
+ *
687
+ * @param {object} envelope - envelope with `multisig_provenance` to check
688
+ * @param {{ m: number, members?: Array<{did:string}> }} [policy] - optional
689
+ * policy spec; when present, signer-membership and threshold are checked
690
+ * against it instead of provenance's own `threshold_m` field
691
+ * @returns {{ ok: boolean, code?: string, detail?: string }}
692
+ */
693
+ export function verifyMultisigProvenance(envelope, policy) {
694
+ if (!envelope || typeof envelope !== "object") {
695
+ return { ok: false, code: "BAD_ENVELOPE" };
696
+ }
697
+ const prov = envelope.multisig_provenance;
698
+ if (!prov || typeof prov !== "object") {
699
+ return { ok: false, code: "MISSING_PROVENANCE" };
700
+ }
701
+ for (const f of MULTISIG_PROVENANCE_REQUIRED_FIELDS) {
702
+ if (prov[f] === undefined || prov[f] === null) {
703
+ return { ok: false, code: "INCOMPLETE_PROVENANCE", detail: f };
704
+ }
705
+ }
706
+ if (typeof prov.proposal_id !== "string" || !prov.proposal_id) {
707
+ return { ok: false, code: "BAD_PROPOSAL_ID" };
708
+ }
709
+ if (
710
+ !Number.isInteger(prov.threshold_m) ||
711
+ prov.threshold_m < 1 ||
712
+ !Number.isInteger(prov.member_count_n) ||
713
+ prov.member_count_n < prov.threshold_m
714
+ ) {
715
+ return { ok: false, code: "BAD_THRESHOLD" };
716
+ }
717
+ if (!Array.isArray(prov.signers) || !Array.isArray(prov.partial_sigs)) {
718
+ return { ok: false, code: "BAD_PROVENANCE_SHAPE" };
719
+ }
720
+ if (prov.signers.length !== prov.partial_sigs.length) {
721
+ return { ok: false, code: "SIGNERS_SIGS_LENGTH_MISMATCH" };
722
+ }
723
+ const requiredM =
724
+ policy && Number.isInteger(policy.m) ? policy.m : prov.threshold_m;
725
+ if (prov.partial_sigs.length < requiredM) {
726
+ return {
727
+ ok: false,
728
+ code: "BELOW_THRESHOLD",
729
+ detail: `${prov.partial_sigs.length} < ${requiredM}`,
730
+ };
731
+ }
732
+ const sorted = [...prov.signers].sort();
733
+ for (let i = 0; i < prov.signers.length; i++) {
734
+ if (prov.signers[i] !== sorted[i]) {
735
+ return { ok: false, code: "SIGNERS_NOT_SORTED" };
736
+ }
737
+ }
738
+ const allowed =
739
+ policy && Array.isArray(policy.members)
740
+ ? new Set(policy.members.map((m) => m.did))
741
+ : null;
742
+ for (let i = 0; i < prov.partial_sigs.length; i++) {
743
+ const s = prov.partial_sigs[i];
744
+ if (!s || typeof s !== "object") {
745
+ return { ok: false, code: "BAD_PARTIAL_SIG", detail: `index ${i}` };
746
+ }
747
+ if (typeof s.did !== "string" || !s.did) {
748
+ return { ok: false, code: "BAD_PARTIAL_SIG_DID", detail: `index ${i}` };
749
+ }
750
+ if (s.did !== prov.signers[i]) {
751
+ return {
752
+ ok: false,
753
+ code: "SIGNER_DID_MISMATCH",
754
+ detail: `signers[${i}]=${prov.signers[i]} vs partial_sigs[${i}].did=${s.did}`,
755
+ };
756
+ }
757
+ if (!MULTISIG_SUPPORTED_ALGS.includes(s.alg)) {
758
+ return {
759
+ ok: false,
760
+ code: "UNSUPPORTED_ALG",
761
+ detail: `index ${i} alg=${s.alg}`,
762
+ };
763
+ }
764
+ if (typeof s.sig !== "string" || !/^[0-9a-fA-F]+$/.test(s.sig) || !s.sig) {
765
+ return {
766
+ ok: false,
767
+ code: "BAD_PARTIAL_SIG_HEX",
768
+ detail: `index ${i}`,
769
+ };
770
+ }
771
+ if (allowed && !allowed.has(s.did)) {
772
+ return {
773
+ ok: false,
774
+ code: "SIGNER_NOT_IN_POLICY",
775
+ detail: s.did,
776
+ };
777
+ }
778
+ }
779
+ return { ok: true };
780
+ }
781
+
571
782
  /**
572
783
  * Build a multi-hop bridge envelope by chaining single-hop envelopes.
573
784
  * For a route A → B → C, the caller supplies the leg envelopes
@@ -579,9 +790,23 @@ const SCHEMA_MULTI_HOP_BRIDGE = "mtc-bridge-multihop/v1";
579
790
  *
580
791
  * @param {Array<object>} legEnvelopes - ≥ 2 single-hop bridge envelopes
581
792
  * @param {{ route_id?: string, total_amount?: string, asset?: string }} [meta]
793
+ * @param {{
794
+ * proposalId: string,
795
+ * thresholdM: number,
796
+ * memberCountN: number,
797
+ * signers: string[],
798
+ * partialSigs: Array<{did:string,alg:string,sig:string}>,
799
+ * }} [multisigProvenance] - #21 B.5 Layer 2 PR2: optional m-of-n provenance
800
+ * carried over from a multisig-gated bridge consume. Attached via
801
+ * `attachMultisigProvenance` so canonical form (strip-all-sigs JCS) and
802
+ * verifier path (`verifyMultisigProvenance`) work uniformly.
582
803
  * @returns {object} multi-hop wrapper envelope
583
804
  */
584
- export function buildMultiHopBridgeEnvelope(legEnvelopes, meta = {}) {
805
+ export function buildMultiHopBridgeEnvelope(
806
+ legEnvelopes,
807
+ meta = {},
808
+ multisigProvenance = null,
809
+ ) {
585
810
  if (!Array.isArray(legEnvelopes) || legEnvelopes.length < 2) {
586
811
  throw new RangeError(
587
812
  "buildMultiHopBridgeEnvelope: need at least 2 leg envelopes",
@@ -616,7 +841,7 @@ export function buildMultiHopBridgeEnvelope(legEnvelopes, meta = {}) {
616
841
  legEnvelopes[0].leaf.src_chain,
617
842
  ...legEnvelopes.map((e) => e.leaf.dst_chain),
618
843
  ];
619
- return {
844
+ const wrapper = {
620
845
  schema: SCHEMA_MULTI_HOP_BRIDGE,
621
846
  route_id:
622
847
  meta.route_id ||
@@ -629,6 +854,10 @@ export function buildMultiHopBridgeEnvelope(legEnvelopes, meta = {}) {
629
854
  asset: meta.asset || null,
630
855
  issued_at: new Date().toISOString(),
631
856
  };
857
+ if (multisigProvenance) {
858
+ return attachMultisigProvenance(wrapper, multisigProvenance);
859
+ }
860
+ return wrapper;
632
861
  }
633
862
 
634
863
  /**
@@ -636,11 +865,30 @@ export function buildMultiHopBridgeEnvelope(legEnvelopes, meta = {}) {
636
865
  * own landmark + the chain path must be continuous + at least one leg
637
866
  * must be the "lock" op (the trigger).
638
867
  *
868
+ * #21 B.5 Layer 2 PR3 — when `wrapper.multisig_provenance` is present OR
869
+ * `options.requireMultisig === true`, also runs `verifyMultisigProvenance`
870
+ * (structural). Result includes nested `multisig_result`. Cryptographic
871
+ * verification of each partial sig against canonical wrapper bytes is
872
+ * deferred (would require per-signer pubkey lookup; see Layer 3).
873
+ *
639
874
  * @param {object} wrapper - multi-hop envelope from buildMultiHopBridgeEnvelope
640
875
  * @param {Array<{landmark: object}>} legCacheEntries - one per leg, in order
641
- * @returns {{ ok: boolean, code?: string, leg_results?: Array<{ok, code?}> }}
876
+ * @param {{
877
+ * expectedMultisigPolicy?: { m: number, members?: Array<{did:string}> },
878
+ * requireMultisig?: boolean,
879
+ * }} [options]
880
+ * @returns {{
881
+ * ok: boolean,
882
+ * code?: string,
883
+ * leg_results?: Array<{ok, code?}>,
884
+ * multisig_result?: { ok: boolean, code?: string, detail?: string },
885
+ * }}
642
886
  */
643
- export function verifyMultiHopBridgeEnvelope(wrapper, legCacheEntries) {
887
+ export function verifyMultiHopBridgeEnvelope(
888
+ wrapper,
889
+ legCacheEntries,
890
+ options = {},
891
+ ) {
644
892
  if (!wrapper || wrapper.schema !== SCHEMA_MULTI_HOP_BRIDGE) {
645
893
  return { ok: false, code: "BAD_MULTIHOP_SCHEMA" };
646
894
  }
@@ -679,8 +927,29 @@ export function verifyMultiHopBridgeEnvelope(wrapper, legCacheEntries) {
679
927
  }
680
928
  }
681
929
  }
682
- const allOk = results.every((r) => r.ok);
683
- return { ok: allOk, leg_results: results };
930
+ const legsOk = results.every((r) => r.ok);
931
+ const hasProvenance = !!wrapper.multisig_provenance;
932
+ const requireMultisig = !!options.requireMultisig;
933
+ if (hasProvenance || requireMultisig) {
934
+ const multisigResult = verifyMultisigProvenance(
935
+ wrapper,
936
+ options.expectedMultisigPolicy,
937
+ );
938
+ if (!multisigResult.ok) {
939
+ return {
940
+ ok: false,
941
+ code: "MULTISIG_PROVENANCE_INVALID",
942
+ leg_results: results,
943
+ multisig_result: multisigResult,
944
+ };
945
+ }
946
+ return {
947
+ ok: legsOk,
948
+ leg_results: results,
949
+ multisig_result: multisigResult,
950
+ };
951
+ }
952
+ return { ok: legsOk, leg_results: results };
684
953
  }
685
954
 
686
955
  // ─────────────────────────────────────────────────────────────────────