chainlesschain 0.161.11 → 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 +4 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AIOps-Cv7DnNSv.js → AIOps-Dn8q-Cu4.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-B1m9njbz.js → ActionButton-BePEVs4O.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-CU5lrpIq.js → Analytics-CKp-5Gi0.js} +1 -1
- package/src/assets/web-panel/assets/AppLayout-BSudqld0.js +3 -0
- package/src/assets/web-panel/assets/{AppLayout-BpbfrB7k.css → AppLayout-DBgtkmP7.css} +1 -1
- package/src/assets/web-panel/assets/{Audit-DUkzhAbX.js → Audit-iefpSeRG.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-L1i5rG4e.js → Backup-Ce43ZIhD.js} +1 -1
- package/src/assets/web-panel/assets/BaseInput-BvGiwRav.js +1 -0
- package/src/assets/web-panel/assets/{Chat-ySuja9_N.js → Chat-BAaQg-Oc.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-5jHkbotn.js → Checkbox-Dvn8uxKO.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-CpTSti2I.js → Codegen-CTcZCayj.js} +1 -1
- package/src/assets/web-panel/assets/{Col-C5SJSGcO.js → Col-CBFpGgb5.js} +1 -1
- package/src/assets/web-panel/assets/{Community-0PMz9hPX.js → Community-DmCYaV9M.js} +1 -1
- package/src/assets/web-panel/assets/Compact-DnOD5uMF.js +1 -0
- package/src/assets/web-panel/assets/{Compliance-BEVqh-i7.js → Compliance-u53e9lg-.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-C_UEYiR5.js → Cowork-CL5ohuXH.js} +1 -1
- package/src/assets/web-panel/assets/{Cron-B4f0_E2W.js → Cron-DubcmRgt.js} +1 -1
- package/src/assets/web-panel/assets/{Crosschain-CZT6FKhl.js → Crosschain-BWXcUXc3.js} +1 -1
- package/src/assets/web-panel/assets/{DID-LDSJdg_E.js → DID-Cw0i8WjE.js} +1 -1
- package/src/assets/web-panel/assets/{Dashboard-CczHBzZy.js → Dashboard-BbofvIWH.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-KJXQPCdN.js → Dropdown-DN6ur5xV.js} +1 -1
- package/src/assets/web-panel/assets/Federation-CA-rIdkY.js +1 -0
- package/src/assets/web-panel/assets/{FormItemContext-CenKDKRv.js → FormItemContext-D_HGU4aA.js} +1 -1
- package/src/assets/web-panel/assets/{Git-Ck3yvIOj.js → Git-CBWFs6Uk.js} +1 -1
- package/src/assets/web-panel/assets/{Governance-C-Wqvp5q.js → Governance-Cx6PTC43.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-D1Q4GQsE.js → Inference-Ubk1VstS.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-6-id5kqA.js → KnowledgeGraph-DTB10MYO.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-BIrBldyi.js → Logs-Tfk4FBVC.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-DuEYH2k1.js → Marketplace-BAsztG4O.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-CJyrjpOa.js → McpTools-DliPfvuK.js} +1 -1
- package/src/assets/web-panel/assets/{Memory-25VBuGGA.js → Memory-hkw6Xwyv.js} +1 -1
- package/src/assets/web-panel/assets/{MobileBridge-DDepR_yK.js → MobileBridge-eeNMFGj6.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-B52jkX63.js → Mtc-BP3Riowm.js} +1 -1
- package/src/assets/web-panel/assets/{MtcAudit-BimrvH_0.js → MtcAudit-DvD7pE7V.js} +6 -6
- 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-D9sqtzgg.js → NLProgramming-1y__Epjg.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-BtaoLR6l.js → Notes-CEvJqMP8.js} +1 -1
- package/src/assets/web-panel/assets/{NotificationSettings-DGc4Pnhc.js → NotificationSettings-wmCYm99r.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-CH8YH1WC.js → Organization-CSoz8gGr.js} +1 -1
- package/src/assets/web-panel/assets/{Overflow-BEfCQNlN.js → Overflow-kf1mnFrM.js} +1 -1
- package/src/assets/web-panel/assets/{OverrideContext-DYxbd09I.js → OverrideContext-x9ZzjLwk.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-Bp_HTJ00.js → P2P-C4GfXJ8Z.js} +1 -1
- package/src/assets/web-panel/assets/{Permissions-UHQF_4gL.js → Permissions-CW1Mn95X.js} +1 -1
- package/src/assets/web-panel/assets/{Pipeline-yCVADMbj.js → Pipeline-CM1ppddn.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-C1oNG_1P.js → Privacy-DuAY5fEm.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-DMW2d6Cb.js → ProjectInit-C72779b6.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-5G8pJeC_.js → ProjectSettings-CAilXWam.js} +1 -1
- package/src/assets/web-panel/assets/Projects-0OrfK90x.css +1 -0
- package/src/assets/web-panel/assets/Projects-DTIlVTkF.js +1 -0
- package/src/assets/web-panel/assets/{Providers-CL_oE5tl.js → Providers-Cf6Yv8bu.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-BV38kJpn.js → QuickAsk-CK1_VVzw.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-SAj9Q6gO.js → Recommend-QBC7KgvR.js} +1 -1
- package/src/assets/web-panel/assets/{Reputation-1kCD0gaB.js → Reputation-ju6lWzMM.js} +1 -1
- package/src/assets/web-panel/assets/Row-DQwipGEm.js +1 -0
- package/src/assets/web-panel/assets/{RssFeed-CAJCjHjh.js → RssFeed-DkLGmeLW.js} +1 -1
- package/src/assets/web-panel/assets/{Search-cvcqK6k5.js → Search-YYhitzkt.js} +1 -1
- package/src/assets/web-panel/assets/{Security-CQg7wTKN.js → Security-DEllaBqe.js} +1 -1
- package/src/assets/web-panel/assets/{Services-CsUFvNqx.js → Services-BgPHBq9S.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-xBd6cpeg.js → Skeleton-DHEyJI7c.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CaroVcXP.js → Skills-c212vgf3.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-BtpCM7Ko.js → Sla-COqhw_no.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-BqMJfygi.js → SpeechSettings-Bmds06su.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-CZouEnTT.js → SyncSettings-Bx1_A34Y.js} +1 -1
- package/src/assets/web-panel/assets/{Tasks-CXRSaKbt.js → Tasks-B0Lo_9aN.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-DJY8TQ5i.js → Templates-D0RcpfuW.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BvR-WVDe.js → Tenant-G0yVctLy.js} +1 -1
- package/src/assets/web-panel/assets/Terminal-DqgLtK3c.js +3 -0
- package/src/assets/web-panel/assets/Terminal-G1DrFtKr.css +1 -0
- package/src/assets/web-panel/assets/{Tokens-BvsTsita.js → Tokens-Cdt0aqCc.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-BpAXwpX5.js → Trigger-DFbfZFuJ.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-CQn3-pr1.js → Trust-BQXsWPz6.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-mdcVVgmW.js → UkeySign-DBem5Xkj.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-B64a6mxs.js → VideoEditing-B1yipKKv.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-CD6sh797.js → Wallet-iPJtU5QZ.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-CBMAhuJk.js → WebAuthn-HLb9srUL.js} +1 -1
- package/src/assets/web-panel/assets/{WorkflowEditor-BL2e4pEf.js → WorkflowEditor-BSqXL8GD.js} +1 -1
- package/src/assets/web-panel/assets/addon-fit-CK6X9sAG.js +1 -0
- package/src/assets/web-panel/assets/{chat-D0R75-Vf.js → chat-D_J-ZVBh.js} +1 -1
- package/src/assets/web-panel/assets/{collapseMotion-CZQhO80h.js → collapseMotion-CjFH_Jop.js} +1 -1
- package/src/assets/web-panel/assets/{colors-Z5R3KUYW.js → colors-B126JcB4.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-Buyen0aR.js → compact-item-Bw5EEKw4.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-D9l1X-OX.js → createContext-xLJXI46O.js} +1 -1
- package/src/assets/web-panel/assets/{echarts-TwhxblyG.js → echarts-Bq-n0MtJ.js} +1 -1
- package/src/assets/web-panel/assets/{hasIn-DV4wtumO.js → hasIn-K8QCNlyy.js} +1 -1
- package/src/assets/web-panel/assets/{icons-Q99MnhG8.js → icons-NT6gy8Ee.js} +2 -2
- package/src/assets/web-panel/assets/{index--pPzkOu2.js → index-85lab9HJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-BSXvEQFS.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-C4m8PjHn.js → index-BQ_NIMQS.js} +1 -1
- package/src/assets/web-panel/assets/{index-BAf0-ayI.js → index-BSgvvXgq.js} +2 -2
- package/src/assets/web-panel/assets/{index-RpJYGfg0.js → index-BWUG9yVI.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cs7GKmFk.js → index-B_PqXUkT.js} +6 -6
- package/src/assets/web-panel/assets/index-BaXXyyT0.js +21 -0
- package/src/assets/web-panel/assets/index-Bptjq-wQ.js +1 -0
- package/src/assets/web-panel/assets/{index-CpemtT_B.js → index-BsbKjK1o.js} +2 -2
- package/src/assets/web-panel/assets/{index-Cs2QeQFi.js → index-Bv9odL87.js} +2 -2
- package/src/assets/web-panel/assets/{index-0P8ZPtKp.js → index-C2CgD7TN.js} +1 -1
- package/src/assets/web-panel/assets/{index-CprQ5h5e.js → index-COCq6xuZ.js} +1 -1
- package/src/assets/web-panel/assets/index-CVeuzjxG.js +55 -0
- package/src/assets/web-panel/assets/index-CWBajOcE.js +3 -0
- package/src/assets/web-panel/assets/{index-CT46jhtC.js → index-CvAey6iD.js} +1 -1
- package/src/assets/web-panel/assets/{index-Z-i_sfIN.js → index-DFPVNSby.js} +1 -1
- package/src/assets/web-panel/assets/{index-CoV9UGnc.js → index-DFwF_JeQ.js} +2 -2
- package/src/assets/web-panel/assets/{index-DO9gd9wz.js → index-DLZZi1Km.js} +1 -1
- package/src/assets/web-panel/assets/{index-BRWO_MUe.js → index-DY5Oc3z6.js} +4 -4
- package/src/assets/web-panel/assets/index-DZWfmuM2.js +1 -0
- package/src/assets/web-panel/assets/{index-CRvOaSuy.js → index-DZcij18m.js} +2 -2
- package/src/assets/web-panel/assets/{index-BLDanIEV.js → index-Da7bZewp.js} +1 -1
- package/src/assets/web-panel/assets/index-DdMzLeDn.js +1 -0
- package/src/assets/web-panel/assets/index-Dj8BNg-z.js +1 -0
- package/src/assets/web-panel/assets/index-DlX09aXr.js +1 -0
- package/src/assets/web-panel/assets/{index-CHs1Rtgi.js → index-DrfiELFa.js} +2 -2
- package/src/assets/web-panel/assets/{index-DMcK4Son.js → index-Dt0Tp8TS.js} +1 -1
- package/src/assets/web-panel/assets/index-DvNCyE3A.js +13 -0
- package/src/assets/web-panel/assets/{index-vSjbNv8F.js → index-LZYGhjVr.js} +1 -1
- package/src/assets/web-panel/assets/{index-AXmFZTsT.js → index-fMwtau_-.js} +2 -2
- package/src/assets/web-panel/assets/{index-D_g5wyjB.js → index-mqHxeING.js} +1 -1
- package/src/assets/web-panel/assets/{index-DBBX0xZN.js → index-mzNWO8Yy.js} +1 -1
- package/src/assets/web-panel/assets/{index-UAkLBWLE.js → index-p3d5_s7V.js} +2 -2
- package/src/assets/web-panel/assets/{index-BBFrPlY4.js → index-qJbfToeQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DtRv6Tqv.js → index-sAfwWh0R.js} +8 -8
- package/src/assets/web-panel/assets/index-xJkPbv0L.js +12 -0
- package/src/assets/web-panel/assets/index-yhA-qP4Y.js +1 -0
- package/src/assets/web-panel/assets/index-zEr6BW9X.js +1 -0
- package/src/assets/web-panel/assets/{initDefaultProps-C5tyEoyp.js → initDefaultProps-CacwmJft.js} +1 -1
- package/src/assets/web-panel/assets/{motion-By5VpSMZ.js → motion-BWrP7Lz6.js} +2 -2
- package/src/assets/web-panel/assets/{move-CNRFLi9k.js → move-imnbiy1n.js} +1 -1
- package/src/assets/web-panel/assets/{omit-DYc7DRhT.js → omit-BlaOv91I.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-D667btUF.js → pickAttrs-DUGKp0ro.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-Ce-Y2-Qq.js → placementArrow-P0ZA4yAK.js} +1 -1
- package/src/assets/web-panel/assets/responsiveObserve-CZz-M9cI.js +1 -0
- package/src/assets/web-panel/assets/{slide-CyKGKfJ8.js → slide-BEnICwoX.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-DffXAKmJ.js → statusUtils-DdYKRSiH.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-CeeSLisW.js → styleChecker-DgUxvOQt.js} +1 -1
- package/src/assets/web-panel/assets/useFlexGapSupport-BUEmeWdR.js +1 -0
- package/src/assets/web-panel/assets/{useFs--Vkrwu51.js → useFs-BXsj6Z5e.js} +1 -1
- package/src/assets/web-panel/assets/{useMergedState-D3PX36GT.js → useMergedState-O7QXt4P5.js} +1 -1
- package/src/assets/web-panel/assets/{useRefs-opfcK7Pm.js → useRefs-0J6m8UWN.js} +1 -1
- package/src/assets/web-panel/assets/{useState-Cp5WlLTS.js → useState-CSzR8F8O.js} +1 -1
- package/src/assets/web-panel/assets/{vendor-BVLz_z7V.js → vendor-M5lGV-wr.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-ChnNrJNw.js → vnode-B3Pp583L.js} +1 -1
- package/src/assets/web-panel/assets/xterm-BZcWGsqw.js +9 -0
- package/src/assets/web-panel/assets/xterm-DFuMZ0ql.css +1 -0
- package/src/assets/web-panel/assets/{zoom-DE1Teifl.js → zoom-D-LYfiQh.js} +1 -1
- package/src/assets/web-panel/index.html +3 -3
- package/src/commands/crosschain.js +403 -1
- package/src/commands/pair.js +291 -0
- package/src/gateways/terminal/PtyManager.js +248 -0
- package/src/gateways/terminal/RingBuffer.js +61 -0
- package/src/gateways/terminal/terminal-handlers.js +121 -0
- package/src/gateways/ws/topic-handler-attachment.js +211 -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/runtime/agent-runtime.js +33 -0
- package/src/assets/web-panel/assets/AppLayout-CrQBcjC1.js +0 -3
- package/src/assets/web-panel/assets/BaseInput-CQSPRZVh.js +0 -1
- package/src/assets/web-panel/assets/Compact-CwrJK_94.js +0 -1
- package/src/assets/web-panel/assets/Federation-Bu085RVF.js +0 -1
- package/src/assets/web-panel/assets/Multisig-D-IuEDLa.css +0 -1
- package/src/assets/web-panel/assets/Multisig-DWsGuuyz.js +0 -1
- package/src/assets/web-panel/assets/Projects-BpLpgCga.js +0 -1
- package/src/assets/web-panel/assets/Projects-C34BWmmy.css +0 -1
- package/src/assets/web-panel/assets/Row-CUF6ofec.js +0 -1
- package/src/assets/web-panel/assets/index-BBvRNeKy.js +0 -1
- package/src/assets/web-panel/assets/index-BJzerWCW.js +0 -1
- package/src/assets/web-panel/assets/index-BbZ_pXN3.js +0 -1
- package/src/assets/web-panel/assets/index-BloaIh8i.js +0 -1
- package/src/assets/web-panel/assets/index-C1u_F_WR.js +0 -12
- package/src/assets/web-panel/assets/index-CHPsWSeL.js +0 -1
- package/src/assets/web-panel/assets/index-CLk9-Adx.js +0 -13
- package/src/assets/web-panel/assets/index-Cyal1Os0.js +0 -1
- package/src/assets/web-panel/assets/index-DYq0yh8a.js +0 -1
- package/src/assets/web-panel/assets/index-KvlSpGws.js +0 -21
- package/src/assets/web-panel/assets/index-WbY1RGjg.js +0 -3
- package/src/assets/web-panel/assets/index-cTf60nCj.js +0 -1
- package/src/assets/web-panel/assets/index-ry4j00pG.js +0 -55
- package/src/assets/web-panel/assets/responsiveObserve-BzzJzAds.js +0 -1
- package/src/assets/web-panel/assets/useFlexGapSupport-CCzODKgI.js +0 -1
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* terminal-handlers — WS topic handlers for Plan A remote-terminal.
|
|
3
|
+
*
|
|
4
|
+
* ESM mirror of `desktop-app-vue/src/main/web-shell/handlers/terminal-handlers.js`.
|
|
5
|
+
* Keep both copies in sync (see the matching mirror docstring there).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_DANGEROUS_PATTERNS = [
|
|
9
|
+
/\brm\s+-rf\b/i,
|
|
10
|
+
/\bformat\s+[a-z]:/i,
|
|
11
|
+
/\bshutdown\b/i,
|
|
12
|
+
/\bdel\s+\/[sq]/i,
|
|
13
|
+
/\bdiskpart\b/i,
|
|
14
|
+
/:\(\)\s*{\s*:\s*\|\s*:\s*&\s*}\s*;\s*:/,
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export function bufferToBase64(buf) {
|
|
18
|
+
return Buffer.isBuffer(buf)
|
|
19
|
+
? buf.toString("base64")
|
|
20
|
+
: Buffer.from(buf, "utf-8").toString("base64");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function base64ToBuffer(s) {
|
|
24
|
+
if (typeof s !== "string") {
|
|
25
|
+
throw new Error("data_must_be_base64_string");
|
|
26
|
+
}
|
|
27
|
+
return Buffer.from(s, "base64");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createTerminalHandlers(options) {
|
|
31
|
+
const { ptyManager, broadcast } = options;
|
|
32
|
+
if (!ptyManager) throw new TypeError("ptyManager is required");
|
|
33
|
+
if (typeof broadcast !== "function") {
|
|
34
|
+
throw new TypeError("broadcast must be a function");
|
|
35
|
+
}
|
|
36
|
+
const dangerousPatterns =
|
|
37
|
+
options.dangerousPatterns || DEFAULT_DANGEROUS_PATTERNS;
|
|
38
|
+
const requireConfirmation =
|
|
39
|
+
options.requireConfirmation || (async () => false);
|
|
40
|
+
const verifyTrustedSource = options.verifyTrustedSource || (() => true);
|
|
41
|
+
|
|
42
|
+
function attachServerEvents() {
|
|
43
|
+
ptyManager.on("stdout", ({ sessionId, data, seq }) => {
|
|
44
|
+
broadcast({
|
|
45
|
+
type: "terminal.stdout",
|
|
46
|
+
payload: { sessionId, data: bufferToBase64(data), seq },
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
ptyManager.on("exit", ({ sessionId, exitCode, signal }) => {
|
|
50
|
+
broadcast({
|
|
51
|
+
type: "terminal.exit",
|
|
52
|
+
payload: { sessionId, exitCode, signal },
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const handlers = {
|
|
58
|
+
"terminal.create": async (frame) => {
|
|
59
|
+
if (!verifyTrustedSource(frame)) return null;
|
|
60
|
+
const payload = frame?.payload || frame || {};
|
|
61
|
+
return ptyManager.create({
|
|
62
|
+
shell: payload.shell,
|
|
63
|
+
cwd: payload.cwd,
|
|
64
|
+
env: payload.env,
|
|
65
|
+
cols: payload.cols,
|
|
66
|
+
rows: payload.rows,
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
"terminal.list": async (frame) => {
|
|
71
|
+
if (!verifyTrustedSource(frame)) return null;
|
|
72
|
+
return { sessions: ptyManager.list() };
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
"terminal.stdin": async (frame) => {
|
|
76
|
+
if (!verifyTrustedSource(frame)) return null;
|
|
77
|
+
const { sessionId, data } = frame?.payload || frame || {};
|
|
78
|
+
if (!sessionId) throw new Error("session_id_required");
|
|
79
|
+
const buf = base64ToBuffer(data);
|
|
80
|
+
const text = buf.toString("utf-8");
|
|
81
|
+
if (dangerousPatterns.some((re) => re.test(text))) {
|
|
82
|
+
const ok = await requireConfirmation(text, sessionId);
|
|
83
|
+
if (!ok) throw new Error("dangerous_keyword_blocked");
|
|
84
|
+
}
|
|
85
|
+
ptyManager.write(sessionId, buf);
|
|
86
|
+
return { ok: true };
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
"terminal.resize": async (frame) => {
|
|
90
|
+
if (!verifyTrustedSource(frame)) return null;
|
|
91
|
+
const { sessionId, cols, rows } = frame?.payload || frame || {};
|
|
92
|
+
if (!sessionId) throw new Error("session_id_required");
|
|
93
|
+
ptyManager.resize(sessionId, cols, rows);
|
|
94
|
+
return { ok: true };
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
"terminal.close": async (frame) => {
|
|
98
|
+
if (!verifyTrustedSource(frame)) return null;
|
|
99
|
+
const { sessionId } = frame?.payload || frame || {};
|
|
100
|
+
if (!sessionId) throw new Error("session_id_required");
|
|
101
|
+
ptyManager.close(sessionId);
|
|
102
|
+
return { ok: true };
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
"terminal.history": async (frame) => {
|
|
106
|
+
if (!verifyTrustedSource(frame)) return null;
|
|
107
|
+
const { sessionId, fromSeq } = frame?.payload || frame || {};
|
|
108
|
+
if (!sessionId) throw new Error("session_id_required");
|
|
109
|
+
const { chunks, truncated } = ptyManager.history(sessionId, fromSeq || 0);
|
|
110
|
+
return {
|
|
111
|
+
chunks: chunks.map((c) => ({
|
|
112
|
+
seq: c.seq,
|
|
113
|
+
data: bufferToBase64(c.data),
|
|
114
|
+
})),
|
|
115
|
+
truncated,
|
|
116
|
+
};
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return { handlers, attachServerEvents };
|
|
121
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* topic-handler-attachment — wrap a `ChainlessChainWSServer` instance with
|
|
3
|
+
* custom topic handlers, mirroring the existing dispatcher.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from `desktop-app-vue/src/main/web-shell/ws-cli-loader.js` so
|
|
6
|
+
* both consumers (desktop web-shell AND `cc ui`) get the same protocol
|
|
7
|
+
* features:
|
|
8
|
+
*
|
|
9
|
+
* - id + auth gates (token-protected servers reject unauthenticated
|
|
10
|
+
* custom-topic calls; native CLI dispatch is untouched)
|
|
11
|
+
* - plain async handler → `{type:"<topic>.result", ok, result}`
|
|
12
|
+
* - async generator handler → `{type:"<topic>.chunk", ok, chunk}` *N +
|
|
13
|
+
* terminal `{type:"<topic>.result", ok, result}`
|
|
14
|
+
* - `<topic>.cancel` frame → `gen.return("client_cancel")` for in-flight
|
|
15
|
+
* streams (keyed by request id, not topic)
|
|
16
|
+
* - `ws.on("close")` → unwind every stream the client owned
|
|
17
|
+
*
|
|
18
|
+
* Single call site contract:
|
|
19
|
+
*
|
|
20
|
+
* const handle = attachTopicHandlers(server, {
|
|
21
|
+
* handlers: { 'terminal.create': async (frame, ctx) => {...}, ... },
|
|
22
|
+
* });
|
|
23
|
+
* handle.register('extra.topic', fn);
|
|
24
|
+
* handle.broadcast({type:'terminal.stdout', payload:{...}});
|
|
25
|
+
*
|
|
26
|
+
* Returns `{ register, broadcast, topicHandlers }`. The server's
|
|
27
|
+
* `_dispatcher` is mutated in place — calling this twice on the same
|
|
28
|
+
* server stacks wrappers (each falls through to the previous), which is
|
|
29
|
+
* fine but usually unnecessary.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {(frame: any, ctx: { topic: string, id: string, server: any, ws: any, clientId: string }) => (Promise<any> | any | AsyncIterable<any>)} TopicHandler
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {any} server ChainlessChainWSServer instance (post-start())
|
|
38
|
+
* @param {object} [opts]
|
|
39
|
+
* @param {Record<string, TopicHandler>} [opts.handlers]
|
|
40
|
+
* @returns {{
|
|
41
|
+
* register: (topic: string, fn: TopicHandler) => void,
|
|
42
|
+
* broadcast: (frame: any) => void,
|
|
43
|
+
* topicHandlers: Map<string, TopicHandler>,
|
|
44
|
+
* }}
|
|
45
|
+
*/
|
|
46
|
+
export function attachTopicHandlers(server, opts = {}) {
|
|
47
|
+
/** @type {Map<string, TopicHandler>} */
|
|
48
|
+
const topicHandlers = new Map();
|
|
49
|
+
if (opts.handlers) {
|
|
50
|
+
for (const [topic, fn] of Object.entries(opts.handlers)) {
|
|
51
|
+
if (typeof fn !== "function") {
|
|
52
|
+
throw new TypeError(
|
|
53
|
+
`attachTopicHandlers: handler for "${topic}" must be a function`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
topicHandlers.set(topic, fn);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** @type {Map<string, { gen: AsyncIterator<any>, clientId: string }>} */
|
|
61
|
+
const inFlightStreams = new Map();
|
|
62
|
+
|
|
63
|
+
async function unwindStream(id, reason) {
|
|
64
|
+
const entry = inFlightStreams.get(id);
|
|
65
|
+
if (!entry) return;
|
|
66
|
+
inFlightStreams.delete(id);
|
|
67
|
+
try {
|
|
68
|
+
if (typeof entry.gen.return === "function") {
|
|
69
|
+
await entry.gen.return(reason);
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
// Generator throw on unwind is non-fatal.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const wsCloseHooked = new WeakSet();
|
|
77
|
+
function ensureWsCloseHook(ws, clientId) {
|
|
78
|
+
if (!ws || typeof ws.on !== "function") return;
|
|
79
|
+
if (wsCloseHooked.has(ws)) return;
|
|
80
|
+
wsCloseHooked.add(ws);
|
|
81
|
+
ws.on("close", () => {
|
|
82
|
+
for (const [id, entry] of inFlightStreams) {
|
|
83
|
+
if (entry.clientId === clientId) {
|
|
84
|
+
unwindStream(id, "ws_close").catch(() => {});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const originalDispatcher = server._dispatcher;
|
|
91
|
+
server._dispatcher = {
|
|
92
|
+
async dispatch(clientId, ws, message) {
|
|
93
|
+
const { id, type } = message || {};
|
|
94
|
+
ensureWsCloseHook(ws, clientId);
|
|
95
|
+
|
|
96
|
+
if (id && typeof type === "string" && type.endsWith(".cancel")) {
|
|
97
|
+
await unwindStream(id, "client_cancel");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (id && type && topicHandlers.has(type)) {
|
|
102
|
+
const client = server.clients.get(clientId);
|
|
103
|
+
if (server.token && (!client || !client.authenticated)) {
|
|
104
|
+
server._send(ws, {
|
|
105
|
+
id,
|
|
106
|
+
type: "error",
|
|
107
|
+
code: "AUTH_REQUIRED",
|
|
108
|
+
message: "Authentication required. Send an auth message first.",
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const handler = topicHandlers.get(type);
|
|
114
|
+
try {
|
|
115
|
+
const ret = handler(message, {
|
|
116
|
+
topic: type,
|
|
117
|
+
id,
|
|
118
|
+
server,
|
|
119
|
+
ws,
|
|
120
|
+
clientId,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (ret && typeof ret[Symbol.asyncIterator] === "function") {
|
|
124
|
+
inFlightStreams.set(id, { gen: ret, clientId });
|
|
125
|
+
let finalReturn = null;
|
|
126
|
+
try {
|
|
127
|
+
for (;;) {
|
|
128
|
+
const step = await ret.next();
|
|
129
|
+
if (step.done) {
|
|
130
|
+
finalReturn = step.value ?? null;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
if (ws.readyState !== 1) {
|
|
134
|
+
await unwindStream(id, "readyState_closed");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
server._send(ws, {
|
|
138
|
+
id,
|
|
139
|
+
type: `${type}.chunk`,
|
|
140
|
+
ok: true,
|
|
141
|
+
chunk: step.value ?? null,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
server._send(ws, {
|
|
145
|
+
id,
|
|
146
|
+
type: `${type}.result`,
|
|
147
|
+
ok: true,
|
|
148
|
+
result: finalReturn,
|
|
149
|
+
});
|
|
150
|
+
} catch (streamErr) {
|
|
151
|
+
server._send(ws, {
|
|
152
|
+
id,
|
|
153
|
+
type: `${type}.result`,
|
|
154
|
+
ok: false,
|
|
155
|
+
error:
|
|
156
|
+
streamErr instanceof Error
|
|
157
|
+
? streamErr.message
|
|
158
|
+
: String(streamErr),
|
|
159
|
+
});
|
|
160
|
+
} finally {
|
|
161
|
+
inFlightStreams.delete(id);
|
|
162
|
+
}
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const result = await ret;
|
|
167
|
+
server._send(ws, {
|
|
168
|
+
id,
|
|
169
|
+
type: `${type}.result`,
|
|
170
|
+
ok: true,
|
|
171
|
+
result: result ?? null,
|
|
172
|
+
});
|
|
173
|
+
} catch (err) {
|
|
174
|
+
server._send(ws, {
|
|
175
|
+
id,
|
|
176
|
+
type: `${type}.result`,
|
|
177
|
+
ok: false,
|
|
178
|
+
error: err instanceof Error ? err.message : String(err),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return originalDispatcher.dispatch(clientId, ws, message);
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
function register(topic, fn) {
|
|
189
|
+
if (typeof topic !== "string" || !topic) {
|
|
190
|
+
throw new TypeError(
|
|
191
|
+
"attachTopicHandlers: topic must be a non-empty string",
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
if (typeof fn !== "function") {
|
|
195
|
+
throw new TypeError("attachTopicHandlers: handler must be a function");
|
|
196
|
+
}
|
|
197
|
+
topicHandlers.set(topic, fn);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function broadcast(frame) {
|
|
201
|
+
if (!frame || typeof frame !== "object") return;
|
|
202
|
+
try {
|
|
203
|
+
server._broadcast(frame);
|
|
204
|
+
} catch {
|
|
205
|
+
// _send is readyState-guarded inside the server; the only path here
|
|
206
|
+
// would be an internal CLI ws-server bug. Don't crash the caller.
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return { register, broadcast, topicHandlers };
|
|
211
|
+
}
|
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
|
// ─────────────────────────────────────────────────────────────────────
|