chainlesschain 0.161.12 → 0.162.0
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-B1dwBvzW.js → AIOps-Dn8q-Cu4.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-DfR4oLvh.js → ActionButton-BePEVs4O.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-D3ZYGXjr.js → Analytics-CKp-5Gi0.js} +1 -1
- package/src/assets/web-panel/assets/{AppLayout-CsmOoh-7.js → AppLayout-BSudqld0.js} +2 -2
- package/src/assets/web-panel/assets/{Audit-B4gwDm63.js → Audit-iefpSeRG.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-T42uSArV.js → Backup-Ce43ZIhD.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CFi52nMs.js → BaseInput-BvGiwRav.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-D7Vvok1V.js → Chat-BAaQg-Oc.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-Dwaflpww.js → Checkbox-Dvn8uxKO.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-BIqx3G0b.js → Codegen-CTcZCayj.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DzIUYUNu.js → Col-CBFpGgb5.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DKePtzhk.js → Community-DmCYaV9M.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-CirVV9Wq.js → Compact-DnOD5uMF.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-CrXOr0sy.js → Compliance-u53e9lg-.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-BEBP6Tht.js → Cowork-CL5ohuXH.js} +1 -1
- package/src/assets/web-panel/assets/{Cron-k7nNUuqh.js → Cron-DubcmRgt.js} +1 -1
- package/src/assets/web-panel/assets/{Crosschain-CBnX0Dhq.js → Crosschain-BWXcUXc3.js} +1 -1
- package/src/assets/web-panel/assets/{DID-B48FszWS.js → DID-Cw0i8WjE.js} +1 -1
- package/src/assets/web-panel/assets/{Dashboard-Pb3qfFpp.js → Dashboard-BbofvIWH.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-pUHy4CQ2.js → Dropdown-DN6ur5xV.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-CT0Qs-kR.js → Federation-CA-rIdkY.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-CbJJp5BR.js → FormItemContext-D_HGU4aA.js} +1 -1
- package/src/assets/web-panel/assets/{Git-B3mGNLQe.js → Git-CBWFs6Uk.js} +1 -1
- package/src/assets/web-panel/assets/{Governance-BKf4733q.js → Governance-Cx6PTC43.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-DZjU541G.js → Inference-Ubk1VstS.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-C8L-7Dd1.js → KnowledgeGraph-DTB10MYO.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-CwDwOiKv.js → Logs-Tfk4FBVC.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-C2YWWU0M.js → Marketplace-BAsztG4O.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-dIbkOypF.js → McpTools-DliPfvuK.js} +1 -1
- package/src/assets/web-panel/assets/{Memory-7eF8WzcY.js → Memory-hkw6Xwyv.js} +1 -1
- package/src/assets/web-panel/assets/{MobileBridge-C74GHLbX.js → MobileBridge-eeNMFGj6.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-CSEDo5Fo.js → Mtc-BP3Riowm.js} +1 -1
- package/src/assets/web-panel/assets/{MtcAudit-DiJXxOrB.js → MtcAudit-DvD7pE7V.js} +1 -1
- package/src/assets/web-panel/assets/Multisig-CPtc-fVL.js +7 -0
- package/src/assets/web-panel/assets/Multisig-kwPDnXnl.css +1 -0
- package/src/assets/web-panel/assets/{NLProgramming-DjF-gIUw.js → NLProgramming-1y__Epjg.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-BUE5CvMO.js → Notes-CEvJqMP8.js} +1 -1
- package/src/assets/web-panel/assets/{NotificationSettings-Dfbrobje.js → NotificationSettings-wmCYm99r.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-C6YvqjQB.js → Organization-CSoz8gGr.js} +1 -1
- package/src/assets/web-panel/assets/{Overflow-BvHNhdMR.js → Overflow-kf1mnFrM.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BO0hQHFS.js → P2P-C4GfXJ8Z.js} +1 -1
- package/src/assets/web-panel/assets/{Permissions-CCPlrJeP.js → Permissions-CW1Mn95X.js} +1 -1
- package/src/assets/web-panel/assets/{Pipeline-DTCL3FjJ.js → Pipeline-CM1ppddn.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-08DYgOe_.js → Privacy-DuAY5fEm.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-B7j-Z8sa.js → ProjectInit-C72779b6.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectSettings-CFqLhV1w.js → ProjectSettings-CAilXWam.js} +1 -1
- package/src/assets/web-panel/assets/{Projects-BPlpx2UN.js → Projects-DTIlVTkF.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-BCBPbVbF.js → Providers-Cf6Yv8bu.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-C8Job6zl.js → QuickAsk-CK1_VVzw.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-DOV_Aje-.js → Recommend-QBC7KgvR.js} +1 -1
- package/src/assets/web-panel/assets/{Reputation-DOgK_dIK.js → Reputation-ju6lWzMM.js} +1 -1
- package/src/assets/web-panel/assets/{Row-8jdU1xYg.js → Row-DQwipGEm.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-B289OgNZ.js → RssFeed-DkLGmeLW.js} +1 -1
- package/src/assets/web-panel/assets/{Search-cr0AndFE.js → Search-YYhitzkt.js} +1 -1
- package/src/assets/web-panel/assets/{Security-w4diykaE.js → Security-DEllaBqe.js} +1 -1
- package/src/assets/web-panel/assets/{Services-dTxAI5cG.js → Services-BgPHBq9S.js} +1 -1
- package/src/assets/web-panel/assets/{Skeleton-B3FiUiRo.js → Skeleton-DHEyJI7c.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-DgtBTAFC.js → Skills-c212vgf3.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-B4Y6bvbL.js → Sla-COqhw_no.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-C8FEvArc.js → SpeechSettings-Bmds06su.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-CHvV5L0m.js → SyncSettings-Bx1_A34Y.js} +1 -1
- package/src/assets/web-panel/assets/{Tasks-B4Py5ORS.js → Tasks-B0Lo_9aN.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-MeTyXM5u.js → Templates-D0RcpfuW.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-Doiz7wyQ.js → Tenant-G0yVctLy.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-DSEAdwWN.js → Terminal-DqgLtK3c.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-BBrtaEKt.js → Tokens-Cdt0aqCc.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-nPvjho27.js → Trigger-DFbfZFuJ.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-BFwPyS_6.js → Trust-BQXsWPz6.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-DFsHoY1J.js → UkeySign-DBem5Xkj.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-FkSKqHE9.js → VideoEditing-B1yipKKv.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-AXo-_4-w.js → Wallet-iPJtU5QZ.js} +1 -1
- package/src/assets/web-panel/assets/{WebAuthn-DoScnQ-4.js → WebAuthn-HLb9srUL.js} +1 -1
- package/src/assets/web-panel/assets/{WorkflowEditor-IEMfIax-.js → WorkflowEditor-BSqXL8GD.js} +1 -1
- package/src/assets/web-panel/assets/{chat-pc1ciH6T.js → chat-D_J-ZVBh.js} +1 -1
- package/src/assets/web-panel/assets/{colors-CjkIkB0e.js → colors-B126JcB4.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-CQZnkP5p.js → compact-item-Bw5EEKw4.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-B1McGnV-.js → createContext-xLJXI46O.js} +1 -1
- package/src/assets/web-panel/assets/{hasIn-CbkA6peP.js → hasIn-K8QCNlyy.js} +1 -1
- package/src/assets/web-panel/assets/{index-ChCCGHwz.js → index-85lab9HJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DYAWIXFt.js → index-B9ztiTnX.js} +1 -1
- package/src/assets/web-panel/assets/index-BEYypR_d.js +1 -0
- package/src/assets/web-panel/assets/{index-wwQ_ZkWN.js → index-BQ_NIMQS.js} +1 -1
- package/src/assets/web-panel/assets/{index-BNLeseG1.js → index-BSgvvXgq.js} +1 -1
- package/src/assets/web-panel/assets/{index-DcSe5y5O.js → index-BWUG9yVI.js} +1 -1
- package/src/assets/web-panel/assets/{index-uWvLy_3T.js → index-B_PqXUkT.js} +1 -1
- package/src/assets/web-panel/assets/{index-CJr12ypE.js → index-BaXXyyT0.js} +1 -1
- package/src/assets/web-panel/assets/{index-NRWBOo4F.js → index-Bptjq-wQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dzfj71tf.js → index-BsbKjK1o.js} +1 -1
- package/src/assets/web-panel/assets/{index-DMjPzhsp.js → index-Bv9odL87.js} +1 -1
- package/src/assets/web-panel/assets/{index-5c4JzwY3.js → index-C2CgD7TN.js} +1 -1
- package/src/assets/web-panel/assets/{index-CzZ3LxPK.js → index-COCq6xuZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dn7SHV2e.js → index-CVeuzjxG.js} +1 -1
- package/src/assets/web-panel/assets/{index-DGb36exe.js → index-CWBajOcE.js} +1 -1
- package/src/assets/web-panel/assets/{index-C5mpCgak.js → index-CvAey6iD.js} +1 -1
- package/src/assets/web-panel/assets/{index-CKledOCh.js → index-DFPVNSby.js} +1 -1
- package/src/assets/web-panel/assets/{index-B8r6OBuk.js → index-DFwF_JeQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DjxVsyn_.js → index-DLZZi1Km.js} +1 -1
- package/src/assets/web-panel/assets/{index-DPzT5L14.js → index-DY5Oc3z6.js} +3 -3
- package/src/assets/web-panel/assets/index-DZWfmuM2.js +1 -0
- package/src/assets/web-panel/assets/{index-CFCQl1hk.js → index-DZcij18m.js} +1 -1
- package/src/assets/web-panel/assets/{index-D4rhVRSV.js → index-Da7bZewp.js} +1 -1
- package/src/assets/web-panel/assets/{index-C052wi94.js → index-DdMzLeDn.js} +1 -1
- package/src/assets/web-panel/assets/{index-BTyhXFDW.js → index-Dj8BNg-z.js} +1 -1
- package/src/assets/web-panel/assets/{index-DHtiecyM.js → index-DlX09aXr.js} +1 -1
- package/src/assets/web-panel/assets/{index-DjM0jsm1.js → index-DrfiELFa.js} +1 -1
- package/src/assets/web-panel/assets/{index-CGke5qZp.js → index-Dt0Tp8TS.js} +1 -1
- package/src/assets/web-panel/assets/{index-CKwkP66s.js → index-DvNCyE3A.js} +1 -1
- package/src/assets/web-panel/assets/{index-DWq64zmv.js → index-LZYGhjVr.js} +1 -1
- package/src/assets/web-panel/assets/{index-Di5tuS0d.js → index-fMwtau_-.js} +1 -1
- package/src/assets/web-panel/assets/{index-CEENN81t.js → index-mqHxeING.js} +1 -1
- package/src/assets/web-panel/assets/{index-DHpwwlmv.js → index-mzNWO8Yy.js} +1 -1
- package/src/assets/web-panel/assets/{index-DJWYN-AT.js → index-p3d5_s7V.js} +1 -1
- package/src/assets/web-panel/assets/{index-C0Lj7Yl0.js → index-qJbfToeQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-fqI1KnP_.js → index-sAfwWh0R.js} +1 -1
- package/src/assets/web-panel/assets/{index-DsChgzu2.js → index-xJkPbv0L.js} +1 -1
- package/src/assets/web-panel/assets/{index-CJbqE5Sw.js → index-yhA-qP4Y.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bs-romIz.js → index-zEr6BW9X.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-BTloFqyf.js → initDefaultProps-CacwmJft.js} +1 -1
- package/src/assets/web-panel/assets/{motion-44iMBc1o.js → motion-BWrP7Lz6.js} +1 -1
- package/src/assets/web-panel/assets/{move-CyWQI5eW.js → move-imnbiy1n.js} +1 -1
- package/src/assets/web-panel/assets/{omit-CXfJtuFy.js → omit-BlaOv91I.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-ANFpSZes.js → pickAttrs-DUGKp0ro.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-CigX-url.js → placementArrow-P0ZA4yAK.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-Ch48RA0m.js → responsiveObserve-CZz-M9cI.js} +1 -1
- package/src/assets/web-panel/assets/{slide-BEz3LORF.js → slide-BEnICwoX.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-D9EniG8V.js → statusUtils-DdYKRSiH.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-C1IvuY3L.js → styleChecker-DgUxvOQt.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-C92oHeXB.js → useFlexGapSupport-BUEmeWdR.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-CJhvFpgB.js → useFs-BXsj6Z5e.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-BPkyunN_.js → vnode-B3Pp583L.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-B6Ipb64r.js → zoom-D-LYfiQh.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/crosschain.js +403 -1
- package/src/commands/pair.js +291 -0
- package/src/index.js +2 -0
- package/src/lib/cross-chain-mtc.js +275 -6
- package/src/lib/cross-chain.js +48 -2
- package/src/lib/lan-pairing-preflight.js +425 -0
- package/src/lib/lan-pairing-tokens.js +264 -0
- package/src/assets/web-panel/assets/Multisig-D-IuEDLa.css +0 -1
- package/src/assets/web-panel/assets/Multisig-FZTU5ri6.js +0 -1
- package/src/assets/web-panel/assets/index-BXfePRef.js +0 -1
- 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(
|
|
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
|
-
|
|
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
|
-
* @
|
|
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(
|
|
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
|
|
683
|
-
|
|
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
|
// ─────────────────────────────────────────────────────────────────────
|