chainlesschain 0.166.4 → 0.166.6
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/README.md +32 -0
- package/package.json +2 -1
- package/src/assets/web-panel/assets/{AIOps-HcOh0ZbH.js → AIOps-CVnFjTfG.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-DWjDqBhd.js → ActionButton-BxkwuuQC.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-C6FS-y3W.js → Analytics-C2670q6z.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-DCN5cldF.js → AppLayout-CU_rylW6.js} +4 -4
- package/src/assets/web-panel/assets/{Artifacts-Crd-KE8P.js → Artifacts-DGJmYqnj.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-BexuTQZc.js → Audit-BZOQbINJ.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-Dydkh-b5.js → BackgroundAgents-DghB4QwF.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-BYq21wMP.js → Backup-l0mwPbta.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CmNZP6yM.js → BaseInput-Bv3WSZOh.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-J6bD0zCl.js → Chat-s88nBdvt.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-MEoaygnL.js → ChatBubbleRenderer-Z8CBuB1a.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-Ce-9vNmK.js → Checkbox-B5icasEs.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-zf7-Y-Eh.js → Codegen-CsUFqNud.js} +1 -1
- package/src/assets/web-panel/assets/{Col-D-TkY0FD.js → Col-DlBtClaW.js} +1 -1
- package/src/assets/web-panel/assets/{Community-l300NA-1.js → Community-xRn1IdLB.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-D-9Q_hxc.js → Compact-naIr8a_o.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-DAJ8lLiQ.js → Compliance-Dp9esfFE.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-xmzZ967S.js → Cowork-DQlMYJB-.js} +4 -4
- package/src/assets/web-panel/assets/{Cron-CeGqO5W4.js → Cron-Cd0oNLzJ.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-DXN0AE-4.js → Crosschain-BtDIx3XX.js} +1 -1
- package/src/assets/web-panel/assets/{DID-CtIKASPr.js → DID-BOlpzEwg.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-DqRwT8NX.js → Dashboard-DV15Axk1.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-ddIVljO1.js → Dropdown-COXrewN1.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-CcsFzV4y.js → EmailListRenderer-EjIt0Cm7.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-PnlAUGcG.js → FamilyGuardDashboard-DJlhwb-R.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-BlmLuQrL.js → Federation-BMiFl82k.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-dDovBj1Y.js → FormItemContext-BUcDQyxn.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-DTvGshFr.js → GenericCardRenderer-C25uFxvK.js} +1 -1
- package/src/assets/web-panel/assets/{Git-DuFR2xgA.js → Git-D20uaEUU.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-9EaNcJuq.js → Governance-CQxYJjCX.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-B-iYWH_r.js → Inference-m-sczpS5.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-BGBaKiLX.js → KnowledgeGraph-CBsIlU0S.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-G3wmXzfa.js → Logs-D0-5bqeu.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-hk0YwjTR.js → MarkdownRenderer-BwH414Ms.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-BNARBeAv.js → Marketplace-CwDGFu2Q.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DST2cSSK.js → McpTools-7FxJ3omT.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-piwDLsVH.js → Memory-DOVyb59U.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-D-W8zHdm.js → MobileBridge-DZXl-Hje.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-CwaYHlVP.js → MobileProjects-DlZ8tRXt.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-C2dxnN7Q.js → Mtc-D7Pjx1s_.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-BwPZsV3N.js → MtcAudit-BND7lX8p.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-CUt8w2al.js → Multisig-BknS7B-p.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-DwxErx27.js → NLProgramming-7C2-KPHt.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-B-mXAZ84.js → Notes-DNWdTndN.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-Bd1fBAzd.js → NotificationSettings-DCm4KcJZ.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-CCInnavw.js → OrderTableRenderer-DyVwwjJx.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-DTTONYZT.js → Organization-Dl2O_qJ9.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-H6lCZ1rE.js → Overflow-BOFCvk9Q.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-DyBZzeYW.js → P2P-DgdVowY1.js} +2 -2
- package/src/assets/web-panel/assets/PdhVaultBrowser-BiaZfQIM.js +7 -0
- package/src/assets/web-panel/assets/{Permissions-B5nSQwsr.js → Permissions-BHNEUUwa.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-CACaQtlY.js → PersonalDataHub-CaZbrOdz.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-5y-DR_Bw.js → Pipeline-DJDnoXmE.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-BMSA3Ehd.js → Privacy-CBQr2eX8.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-VCOj1dTa.js → ProjectInit-Yghqa06z.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-CJaXO_iE.js → ProjectSettings-HBAtJi8U.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-C21-MQ3H.js → Projects-DiCGnMjz.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-DZ1FNKUR.js → Providers-uJTz77GI.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-6DKc9UBX.js → QrScannerModal-k3M5ouTp.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-Dyd0Ckrk.js → QuickAsk-CMji45PH.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-CYW4-GJH.js → Recommend-DyoZYPls.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-DLrfPy4F.js → RemoteSession-s1gR38IO.js} +3 -3
- package/src/assets/web-panel/assets/{Reputation-DfqPo1FI.js → Reputation-D4hIs4vV.js} +1 -1
- package/src/assets/web-panel/assets/{Row-CR89H2x5.js → Row-mzE-ElTg.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-B9QY7R-F.js → RssFeed-cg7J3hI6.js} +3 -3
- package/src/assets/web-panel/assets/{Search-CUDnzu4w.js → Search-6M0TkGSZ.js} +1 -1
- package/src/assets/web-panel/assets/{Security-CJ0CtM_Z.js → Security-B1GQ67qh.js} +3 -3
- package/src/assets/web-panel/assets/{Services-CyrQ6F5U.js → Services-D_RwU_5S.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DcwnKatQ.js → Skeleton-TZjk-dsW.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-BhKkYF3_.js → Skills-CG6FcaCV.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-CWh-awh7.js → Sla-BI190QDv.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-BaUMNzx7.js → SpeechSettings-BWmxF2Wu.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-D1Ijwwxl.js → SyncSettings-CwPiGw6L.js} +2 -2
- package/src/assets/web-panel/assets/Tasks-BIXgCeSk.js +1 -0
- package/src/assets/web-panel/assets/{Templates-Ct-9_ki3.js → Templates-CQFgGCoK.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-zBkgyilh.js → Tenant-DAelEdkL.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-VVO2Hjfi.js → Terminal-BbqWK6qh.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-CX9pIkx3.js → TimelineRenderer-cmG3zvY5.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-DHaD6e_m.js → Tokens-C8444NKU.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-CFq9j3WV.js → Trigger-C1Rpnz0I.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DuF7gIYX.js → Trust-u4koPiFD.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-DbuidXJX.js → UkeySign-oPWKn80Z.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-B8IA9_qE.js → VideoEditing-D0CUUY_5.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-DdCglSIK.js → Wallet-fu9pIAbt.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-CeteRsA8.js → WebAuthn-Bc5ciuOx.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-DJhIbG__.js → WorkflowEditor-BCin4_4N.js} +1 -1
- package/src/assets/web-panel/assets/{chat-srOi_Wef.js → chat-B2jbM-XN.js} +1 -1
- package/src/assets/web-panel/assets/{colors-HpVHsNGK.js → colors-CUVoUQ_2.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-DsLhrqWe.js → compact-item-DgOGpkRH.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-aOasIUuE.js → createContext-DIxU6eKI.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DiSlgCC4.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-D5BVMimS.js → hasIn-DoKRKMav.js} +1 -1
- package/src/assets/web-panel/assets/{index-DQ5j1VKg.js → index-2flsPFIX.js} +1 -1
- package/src/assets/web-panel/assets/index-9WIQEHvv.js +1 -0
- package/src/assets/web-panel/assets/{index-D1xZMQrN.js → index-B-f6l4Vm.js} +1 -1
- package/src/assets/web-panel/assets/{index-q--LtRh9.js → index-B3F_p8NA.js} +1 -1
- package/src/assets/web-panel/assets/{index-CQEoGqMG.js → index-B5ySiGYy.js} +1 -1
- package/src/assets/web-panel/assets/index-B7mDobAl.js +1 -0
- package/src/assets/web-panel/assets/{index-DiXsis7m.js → index-B9mSYovv.js} +1 -1
- package/src/assets/web-panel/assets/{index-CUUs12L9.js → index-BCLeNqIL.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dpj-rEoV.js → index-BFUprhNd.js} +1 -1
- package/src/assets/web-panel/assets/{index-b0Y0hDhX.js → index-BRMEO2Tl.js} +1 -1
- package/src/assets/web-panel/assets/{index-CQXp_uNd.js → index-BXb-Smmm.js} +1 -1
- package/src/assets/web-panel/assets/{index-P6IMLmLT.js → index-BtSPhEyC.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bu6TVdgQ.js → index-BwYofQJg.js} +1 -1
- package/src/assets/web-panel/assets/{index-CGVHW0nF.js → index-ByjrulIZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-V0gpVTbS.js → index-C846lPGD.js} +1 -1
- package/src/assets/web-panel/assets/{index-BGBvkNN3.js → index-C88m9lsi.js} +1 -1
- package/src/assets/web-panel/assets/{index-dPFzlII1.js → index-C9TsO9T9.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dum_r_oY.js → index-CF80xxPG.js} +1 -1
- package/src/assets/web-panel/assets/{index-CVp_dHCs.js → index-CLdpHe6w.js} +1 -1
- package/src/assets/web-panel/assets/{index-LSz0ZOB8.js → index-CPXdBnGi.js} +1 -1
- package/src/assets/web-panel/assets/{index-BNbsjeEc.js → index-CWbL1-N0.js} +1 -1
- package/src/assets/web-panel/assets/{index-CSxJ4oRd.js → index-CeWSw-z5.js} +1 -1
- package/src/assets/web-panel/assets/{index-fZnlPk7H.js → index-Cj-us-D_.js} +1 -1
- package/src/assets/web-panel/assets/{index-CvhVCHk-.js → index-Co4lDEk-.js} +1 -1
- package/src/assets/web-panel/assets/{index-DcLBzIiI.js → index-CpvyOt5Z.js} +1 -1
- package/src/assets/web-panel/assets/{index-C4tPtHU0.js → index-CqlwIjJx.js} +1 -1
- package/src/assets/web-panel/assets/{index-ChGUPEwS.js → index-Cskz--fd.js} +1 -1
- package/src/assets/web-panel/assets/{index-BD64r0Tx.js → index-D-ufbAJB.js} +1 -1
- package/src/assets/web-panel/assets/{index-BTwDhhvl.js → index-D2yo6ast.js} +1 -1
- package/src/assets/web-panel/assets/{index-lFOfvZDn.js → index-DGTZdZOE.js} +3 -3
- package/src/assets/web-panel/assets/{index-vQ6jy8Ew.js → index-DHugFl3v.js} +1 -1
- package/src/assets/web-panel/assets/{index-DV4y5rUj.js → index-DJMHnbmu.js} +1 -1
- package/src/assets/web-panel/assets/{index-D2XRXkZU.js → index-DpTFx8Xx.js} +1 -1
- package/src/assets/web-panel/assets/{index-BCp70IlO.js → index-Ds8r3Fzb.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ow3SRXRX.js → index-DtyAuGOm.js} +1 -1
- package/src/assets/web-panel/assets/{index-B615KD2o.js → index-be8SonCh.js} +1 -1
- package/src/assets/web-panel/assets/{index-DRCIPsdm.js → index-eTWPaeBI.js} +1 -1
- package/src/assets/web-panel/assets/{index-kYqi6zYp.js → index-eYxUASSG.js} +1 -1
- package/src/assets/web-panel/assets/{index-BigtYchO.js → index-i5MB455u.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-1NU9yNpr.js → initDefaultProps-B4chl7iR.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BZk2hQ6c.js → motion-DruN9h96.js} +1 -1
- package/src/assets/web-panel/assets/{move-BNvublME.js → move-Cl0UtCgU.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-DWk5BUoL.js → mtc-parser-FX2ttw70.js} +1 -1
- package/src/assets/web-panel/assets/{omit-DztYxX3x.js → omit-BDCneY46.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DURL8mtg.js → pickAttrs-DycsEg8b.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-B820R56z.js → placementArrow-Bc-dBuMv.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-DUAkGJL1.js → responsiveObserve--Wwaf8lu.js} +1 -1
- package/src/assets/web-panel/assets/{slide-uZQeNSo6.js → slide-vDmHQqHH.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-CGR1wFVq.js → statusUtils-9c52yv3G.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-BIIPOMsQ.js → styleChecker-BT4BiO6_.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-rIB9c0g2.js → useFlexGapSupport-C7XxWPcP.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-CrJe1BJi.js → useFs-BHoMqh4i.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-DyEoD3Qm.js → usePersonalDataHub-BOLxvsAG.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-CjohPxI0.js → vnode-CA7poeeP.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-C5POL1_6.js → zoom-B_sAn-co.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +1 -1
- package/src/commands/serve.js +62 -9
- package/src/data/changelog.json +26 -0
- package/src/gateways/ws/ws-server.js +392 -33
- package/src/generated/cc-agent-protocol.schema.json +1442 -1
- package/src/lib/agent-ipc-bus.js +453 -91
- package/src/lib/agent-ipc-flow-control.js +200 -0
- package/src/lib/app-server/server.js +3 -1
- package/src/lib/app-server/websocket-transport.js +546 -0
- package/src/lib/public-ide-capability-manifest.js +4 -0
- package/src/runtime/__tests__/coding-agent-events.test.js +36 -0
- package/src/runtime/coding-agent-events.cjs +82 -0
- package/src/assets/web-panel/assets/PdhVaultBrowser-jj7LhT04.js +0 -7
- package/src/assets/web-panel/assets/Tasks-DFqjHi3T.js +0 -1
- package/src/assets/web-panel/assets/devWarning-B_acr36n.js +0 -1
- package/src/assets/web-panel/assets/index-BnLYpdox.js +0 -1
- package/src/assets/web-panel/assets/index-Dzmwsl_v.js +0 -1
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import { createServer as createHttpServer } from "node:http";
|
|
5
|
+
import { createServer as createHttpsServer } from "node:https";
|
|
6
|
+
import { resolve as resolvePath } from "node:path";
|
|
7
|
+
import { createSecureContext } from "node:tls";
|
|
8
|
+
import { WebSocketServer } from "ws";
|
|
9
|
+
|
|
10
|
+
import { BoundedAsyncQueue, QueueOverloadedError } from "./bounded-queue.js";
|
|
11
|
+
import { CcAppServer } from "./server.js";
|
|
12
|
+
import { JSON_RPC_ERROR, JsonRpcError, rpcError } from "./protocol.js";
|
|
13
|
+
|
|
14
|
+
export const APP_SERVER_WEBSOCKET_PROTOCOL =
|
|
15
|
+
"chainlesschain.app-server.experimental.v1";
|
|
16
|
+
export const APP_SERVER_WEBSOCKET_PATH = "/app-server";
|
|
17
|
+
|
|
18
|
+
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "::1", "[::1]", "localhost"]);
|
|
19
|
+
const MAX_TLS_FILE_BYTES = 1024 * 1024;
|
|
20
|
+
|
|
21
|
+
function boundedInteger(value, fallback, minimum = 1, maximum = 1_000_000) {
|
|
22
|
+
const number = Number(value);
|
|
23
|
+
if (!Number.isSafeInteger(number)) return fallback;
|
|
24
|
+
return Math.min(maximum, Math.max(minimum, number));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isLoopbackHost(host) {
|
|
28
|
+
return LOOPBACK_HOSTS.has(
|
|
29
|
+
String(host || "")
|
|
30
|
+
.trim()
|
|
31
|
+
.toLowerCase(),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function validateWebSocketAppServerOptions(options = {}) {
|
|
36
|
+
const host = String(options.host || "127.0.0.1").trim();
|
|
37
|
+
const remote = !isLoopbackHost(host);
|
|
38
|
+
const token = typeof options.token === "string" ? options.token : "";
|
|
39
|
+
const tlsCertPath = options.tlsCertPath
|
|
40
|
+
? resolvePath(options.tlsCertPath)
|
|
41
|
+
: null;
|
|
42
|
+
const tlsKeyPath = options.tlsKeyPath
|
|
43
|
+
? resolvePath(options.tlsKeyPath)
|
|
44
|
+
: null;
|
|
45
|
+
|
|
46
|
+
if (Buffer.byteLength(token, "utf8") < 32) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
"App Server WebSocket binding requires a token of at least 32 bytes",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
if (remote && options.allowRemote !== true) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
"Non-loopback App Server WebSocket binding requires --allow-remote",
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
if (remote && (!tlsCertPath || !tlsKeyPath)) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
"Non-loopback App Server WebSocket binding requires TLS certificate and key",
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if ((tlsCertPath && !tlsKeyPath) || (!tlsCertPath && tlsKeyPath)) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
"App Server WebSocket TLS requires both certificate and key",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return Object.freeze({
|
|
68
|
+
host,
|
|
69
|
+
remote,
|
|
70
|
+
token,
|
|
71
|
+
tlsCertPath,
|
|
72
|
+
tlsKeyPath,
|
|
73
|
+
secure: Boolean(tlsCertPath && tlsKeyPath),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function constantTimeTokenEqual(expected, actual) {
|
|
78
|
+
const expectedDigest = createHash("sha256").update(expected, "utf8").digest();
|
|
79
|
+
const actualDigest = createHash("sha256").update(actual, "utf8").digest();
|
|
80
|
+
return timingSafeEqual(expectedDigest, actualDigest);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parseProtocols(request) {
|
|
84
|
+
return new Set(
|
|
85
|
+
String(request.headers?.["sec-websocket-protocol"] || "")
|
|
86
|
+
.split(",")
|
|
87
|
+
.map((value) => value.trim())
|
|
88
|
+
.filter(Boolean),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function presentedToken(request, protocols) {
|
|
93
|
+
const authorization = String(request.headers?.authorization || "");
|
|
94
|
+
const bearer = /^Bearer\s+(.+)$/iu.exec(authorization)?.[1];
|
|
95
|
+
if (bearer) return bearer;
|
|
96
|
+
const encoded = [...protocols]
|
|
97
|
+
.find((protocol) => protocol.startsWith("bearer."))
|
|
98
|
+
?.slice("bearer.".length);
|
|
99
|
+
if (!encoded) return "";
|
|
100
|
+
try {
|
|
101
|
+
return Buffer.from(encoded, "base64url").toString("utf8");
|
|
102
|
+
} catch {
|
|
103
|
+
return "";
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function wait(milliseconds) {
|
|
108
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function readTlsFile(filePath, label, fsImpl) {
|
|
112
|
+
let descriptor = null;
|
|
113
|
+
try {
|
|
114
|
+
const link = fsImpl.lstatSync(filePath);
|
|
115
|
+
if (link.isSymbolicLink()) {
|
|
116
|
+
throw new Error(`App Server TLS ${label} must not be a symbolic link`);
|
|
117
|
+
}
|
|
118
|
+
descriptor = fsImpl.openSync(
|
|
119
|
+
filePath,
|
|
120
|
+
fsImpl.constants.O_RDONLY | (fsImpl.constants.O_NOFOLLOW || 0),
|
|
121
|
+
);
|
|
122
|
+
const stat = fsImpl.fstatSync(descriptor);
|
|
123
|
+
if (!stat.isFile() || stat.size <= 0 || stat.size > MAX_TLS_FILE_BYTES) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`App Server TLS ${label} must be a non-empty regular file no larger than 1 MiB`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
if (
|
|
129
|
+
label === "private key" &&
|
|
130
|
+
process.platform !== "win32" &&
|
|
131
|
+
(stat.mode & 0o077) !== 0
|
|
132
|
+
) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
"App Server TLS private key must not be accessible by group or other users",
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return fsImpl.readFileSync(descriptor);
|
|
138
|
+
} finally {
|
|
139
|
+
if (descriptor != null) fsImpl.closeSync(descriptor);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export class WebSocketConnectionTransport extends EventEmitter {
|
|
144
|
+
constructor({
|
|
145
|
+
socket,
|
|
146
|
+
maxQueuedMessages = 256,
|
|
147
|
+
maxQueuedBytes = 4 * 1024 * 1024,
|
|
148
|
+
maxBufferedBytes = 2 * 1024 * 1024,
|
|
149
|
+
slowConsumerTimeoutMs = 5_000,
|
|
150
|
+
} = {}) {
|
|
151
|
+
super();
|
|
152
|
+
if (!socket || typeof socket.send !== "function") {
|
|
153
|
+
throw new TypeError("WebSocket transport requires a socket");
|
|
154
|
+
}
|
|
155
|
+
this.socket = socket;
|
|
156
|
+
this.maxBufferedBytes = boundedInteger(maxBufferedBytes, 2 * 1024 * 1024);
|
|
157
|
+
this.slowConsumerTimeoutMs = boundedInteger(
|
|
158
|
+
slowConsumerTimeoutMs,
|
|
159
|
+
5_000,
|
|
160
|
+
1,
|
|
161
|
+
120_000,
|
|
162
|
+
);
|
|
163
|
+
this.queue = new BoundedAsyncQueue({
|
|
164
|
+
maxItems: boundedInteger(maxQueuedMessages, 256),
|
|
165
|
+
maxBytes: boundedInteger(maxQueuedBytes, 4 * 1024 * 1024),
|
|
166
|
+
sizeOf: (frame) => Buffer.byteLength(frame, "utf8"),
|
|
167
|
+
});
|
|
168
|
+
this.closed = false;
|
|
169
|
+
this.failure = null;
|
|
170
|
+
this.pump = this._pump();
|
|
171
|
+
this.pump.catch(() => {});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async _waitUntilWritable(frameBytes) {
|
|
175
|
+
const deadline = Date.now() + this.slowConsumerTimeoutMs;
|
|
176
|
+
while (
|
|
177
|
+
Number(this.socket.bufferedAmount || 0) + frameBytes >
|
|
178
|
+
this.maxBufferedBytes
|
|
179
|
+
) {
|
|
180
|
+
if (Date.now() >= deadline) {
|
|
181
|
+
throw new QueueOverloadedError(
|
|
182
|
+
"App Server WebSocket slow consumer exceeded the buffer deadline",
|
|
183
|
+
{ retryAfterMs: 1_000 },
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
await wait(10);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async _sendFrame(frame) {
|
|
191
|
+
if (this.socket.readyState !== 1) {
|
|
192
|
+
throw new Error("App Server WebSocket is not open");
|
|
193
|
+
}
|
|
194
|
+
const frameBytes = Buffer.byteLength(frame, "utf8");
|
|
195
|
+
await this._waitUntilWritable(frameBytes);
|
|
196
|
+
await new Promise((resolve, reject) => {
|
|
197
|
+
this.socket.send(frame, { binary: false }, (error) =>
|
|
198
|
+
error ? reject(error) : resolve(),
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async _pump() {
|
|
204
|
+
try {
|
|
205
|
+
for await (const frame of this.queue) await this._sendFrame(frame);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
this.failure = error;
|
|
208
|
+
this.queue.close(error);
|
|
209
|
+
this._tripSlowConsumer();
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
_tripSlowConsumer() {
|
|
215
|
+
if (this.socket.readyState === 0 || this.socket.readyState === 1) {
|
|
216
|
+
try {
|
|
217
|
+
this.socket.close(1013, "App Server slow consumer");
|
|
218
|
+
} catch {
|
|
219
|
+
this.socket.terminate?.();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
this.emit("breaker", this.failure);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
send(message) {
|
|
226
|
+
if (this.failure) return Promise.reject(this.failure);
|
|
227
|
+
if (this.closed)
|
|
228
|
+
return Promise.reject(new Error("WebSocket transport closed"));
|
|
229
|
+
const frame = JSON.stringify(message);
|
|
230
|
+
try {
|
|
231
|
+
this.queue.push(frame);
|
|
232
|
+
return Promise.resolve();
|
|
233
|
+
} catch (error) {
|
|
234
|
+
if (error instanceof QueueOverloadedError) {
|
|
235
|
+
this.failure = error;
|
|
236
|
+
this._tripSlowConsumer();
|
|
237
|
+
}
|
|
238
|
+
return Promise.reject(error);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async close() {
|
|
243
|
+
if (this.closed) return;
|
|
244
|
+
this.closed = true;
|
|
245
|
+
this.queue.close();
|
|
246
|
+
await this.pump.catch(() => {});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export class WebSocketAppServerHost extends EventEmitter {
|
|
251
|
+
constructor(options = {}) {
|
|
252
|
+
super();
|
|
253
|
+
this.options = options;
|
|
254
|
+
this.binding = validateWebSocketAppServerOptions(options);
|
|
255
|
+
this.port = boundedInteger(options.port, 18800, 0, 65_535);
|
|
256
|
+
this.path = options.path || APP_SERVER_WEBSOCKET_PATH;
|
|
257
|
+
this.maxConnections = boundedInteger(options.maxConnections, 10, 1, 1_000);
|
|
258
|
+
this.maxPayloadBytes = boundedInteger(options.maxPayloadBytes, 1024 * 1024);
|
|
259
|
+
this.maxPendingReceives = boundedInteger(options.maxPendingReceives, 512);
|
|
260
|
+
this.cleanupTimeoutMs = boundedInteger(
|
|
261
|
+
options.cleanupTimeoutMs,
|
|
262
|
+
10_000,
|
|
263
|
+
1,
|
|
264
|
+
120_000,
|
|
265
|
+
);
|
|
266
|
+
this.store = options.store;
|
|
267
|
+
this.kernelFactory = options.kernelFactory;
|
|
268
|
+
this.serverFactory = options.serverFactory;
|
|
269
|
+
this.WebSocketServerClass = options.WebSocketServerClass || WebSocketServer;
|
|
270
|
+
this.createHttpServer = options.createHttpServer || createHttpServer;
|
|
271
|
+
this.createHttpsServer = options.createHttpsServer || createHttpsServer;
|
|
272
|
+
this.fs = options.fs || fs;
|
|
273
|
+
this.createSecureContext =
|
|
274
|
+
options.createSecureContext || createSecureContext;
|
|
275
|
+
this.connections = new Map();
|
|
276
|
+
this.cleanups = new Set();
|
|
277
|
+
this.httpServer = null;
|
|
278
|
+
this.wss = null;
|
|
279
|
+
this.on("error", () => {});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
_authorize(info, done) {
|
|
283
|
+
const protocols = parseProtocols(info.req);
|
|
284
|
+
if (!protocols.has(APP_SERVER_WEBSOCKET_PROTOCOL)) {
|
|
285
|
+
done(false, 426, "App Server WebSocket subprotocol required");
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (
|
|
289
|
+
this.binding.token &&
|
|
290
|
+
!constantTimeTokenEqual(
|
|
291
|
+
this.binding.token,
|
|
292
|
+
presentedToken(info.req, protocols),
|
|
293
|
+
)
|
|
294
|
+
) {
|
|
295
|
+
done(false, 401, "Unauthorized");
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (this.connections.size >= this.maxConnections) {
|
|
299
|
+
done(false, 503, "Connection limit reached");
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
done(true);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
_createHttpHost() {
|
|
306
|
+
const requestHandler = (_request, response) => {
|
|
307
|
+
response.writeHead(404, {
|
|
308
|
+
"content-type": "text/plain; charset=utf-8",
|
|
309
|
+
"x-content-type-options": "nosniff",
|
|
310
|
+
});
|
|
311
|
+
response.end("Not Found");
|
|
312
|
+
};
|
|
313
|
+
if (!this.binding.secure) return this.createHttpServer(requestHandler);
|
|
314
|
+
const cert = readTlsFile(this.binding.tlsCertPath, "certificate", this.fs);
|
|
315
|
+
const key = readTlsFile(this.binding.tlsKeyPath, "private key", this.fs);
|
|
316
|
+
try {
|
|
317
|
+
this.createSecureContext({ cert, key, minVersion: "TLSv1.2" });
|
|
318
|
+
} catch {
|
|
319
|
+
throw new Error(
|
|
320
|
+
"App Server TLS certificate and private key are invalid or do not match",
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
return this.createHttpsServer(
|
|
324
|
+
{
|
|
325
|
+
cert,
|
|
326
|
+
key,
|
|
327
|
+
minVersion: "TLSv1.2",
|
|
328
|
+
},
|
|
329
|
+
requestHandler,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async start() {
|
|
334
|
+
if (this.httpServer)
|
|
335
|
+
throw new Error("App Server WebSocket already started");
|
|
336
|
+
this.httpServer = this._createHttpHost();
|
|
337
|
+
this.wss = new this.WebSocketServerClass({
|
|
338
|
+
server: this.httpServer,
|
|
339
|
+
path: this.path,
|
|
340
|
+
maxPayload: this.maxPayloadBytes,
|
|
341
|
+
perMessageDeflate: false,
|
|
342
|
+
clientTracking: true,
|
|
343
|
+
verifyClient: (info, done) => this._authorize(info, done),
|
|
344
|
+
handleProtocols: (protocols) =>
|
|
345
|
+
protocols.has(APP_SERVER_WEBSOCKET_PROTOCOL)
|
|
346
|
+
? APP_SERVER_WEBSOCKET_PROTOCOL
|
|
347
|
+
: false,
|
|
348
|
+
});
|
|
349
|
+
this.wss.on("connection", (socket, request) =>
|
|
350
|
+
this._accept(socket, request),
|
|
351
|
+
);
|
|
352
|
+
this.wss.on("error", (error) => this.emit("error", error));
|
|
353
|
+
|
|
354
|
+
await new Promise((resolve, reject) => {
|
|
355
|
+
const onError = (error) => {
|
|
356
|
+
this.httpServer.off("listening", onListening);
|
|
357
|
+
reject(error);
|
|
358
|
+
};
|
|
359
|
+
const onListening = () => {
|
|
360
|
+
this.httpServer.off("error", onError);
|
|
361
|
+
resolve();
|
|
362
|
+
};
|
|
363
|
+
this.httpServer.once("error", onError);
|
|
364
|
+
this.httpServer.once("listening", onListening);
|
|
365
|
+
this.httpServer.listen(this.port, this.binding.host);
|
|
366
|
+
});
|
|
367
|
+
const address = this.httpServer.address();
|
|
368
|
+
const port =
|
|
369
|
+
typeof address === "object" && address ? address.port : this.port;
|
|
370
|
+
this.port = port;
|
|
371
|
+
const displayHost = this.binding.host.includes(":")
|
|
372
|
+
? `[${this.binding.host.replace(/^\[|\]$/gu, "")}]`
|
|
373
|
+
: this.binding.host;
|
|
374
|
+
return Object.freeze({
|
|
375
|
+
host: this.binding.host,
|
|
376
|
+
port,
|
|
377
|
+
path: this.path,
|
|
378
|
+
secure: this.binding.secure,
|
|
379
|
+
stability: "experimental",
|
|
380
|
+
url: `${this.binding.secure ? "wss" : "ws"}://${displayHost}:${port}${this.path}`,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
_accept(socket, request) {
|
|
385
|
+
const transport = new WebSocketConnectionTransport({
|
|
386
|
+
socket,
|
|
387
|
+
maxQueuedMessages: this.options.maxQueuedMessages,
|
|
388
|
+
maxQueuedBytes: this.options.maxQueuedBytes,
|
|
389
|
+
maxBufferedBytes: this.options.maxBufferedBytes,
|
|
390
|
+
slowConsumerTimeoutMs: this.options.slowConsumerTimeoutMs,
|
|
391
|
+
});
|
|
392
|
+
const send = (message) => transport.send(message);
|
|
393
|
+
const server = this.serverFactory
|
|
394
|
+
? this.serverFactory({ send, request, transport: "websocket" })
|
|
395
|
+
: new CcAppServer({
|
|
396
|
+
send,
|
|
397
|
+
store: this.store,
|
|
398
|
+
kernel: this.kernelFactory?.(),
|
|
399
|
+
transport: "websocket",
|
|
400
|
+
maxQueuedRequests: this.options.maxQueuedRequests,
|
|
401
|
+
maxQueuedRequestBytes: this.options.maxQueuedRequestBytes,
|
|
402
|
+
maxConcurrentRequests: this.options.maxConcurrentRequests,
|
|
403
|
+
requestTimeoutMs: this.options.requestTimeoutMs,
|
|
404
|
+
interruptSettlementMs: this.options.interruptSettlementMs,
|
|
405
|
+
});
|
|
406
|
+
const context = {
|
|
407
|
+
socket,
|
|
408
|
+
transport,
|
|
409
|
+
server,
|
|
410
|
+
pending: new Set(),
|
|
411
|
+
closing: false,
|
|
412
|
+
cleanup: null,
|
|
413
|
+
};
|
|
414
|
+
this.connections.set(socket, context);
|
|
415
|
+
socket.on("message", (data, isBinary) =>
|
|
416
|
+
this._receive(context, data, isBinary),
|
|
417
|
+
);
|
|
418
|
+
socket.on("close", () => void this._cleanup(context));
|
|
419
|
+
socket.on("error", (error) => {
|
|
420
|
+
this.emit("connection-error", error);
|
|
421
|
+
void this._cleanup(context);
|
|
422
|
+
});
|
|
423
|
+
transport.on("breaker", () => void this._cleanup(context));
|
|
424
|
+
this.emit("connection", { remoteAddress: request.socket?.remoteAddress });
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
_receive(context, data, isBinary) {
|
|
428
|
+
if (context.closing) return;
|
|
429
|
+
if (isBinary) {
|
|
430
|
+
context.socket.close(1003, "JSON text frames required");
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
const frame = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
434
|
+
if (frame.length > this.maxPayloadBytes) {
|
|
435
|
+
context.socket.close(1009, "App Server frame too large");
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
let message;
|
|
439
|
+
try {
|
|
440
|
+
message = JSON.parse(frame.toString("utf8"));
|
|
441
|
+
} catch {
|
|
442
|
+
this._sendControl(
|
|
443
|
+
context,
|
|
444
|
+
rpcError(
|
|
445
|
+
null,
|
|
446
|
+
new JsonRpcError(JSON_RPC_ERROR.PARSE_ERROR, "Invalid JSON"),
|
|
447
|
+
),
|
|
448
|
+
);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const isClientResponse =
|
|
453
|
+
message?.method == null && Object.hasOwn(message || {}, "id");
|
|
454
|
+
if (!isClientResponse && context.pending.size >= this.maxPendingReceives) {
|
|
455
|
+
this._sendControl(
|
|
456
|
+
context,
|
|
457
|
+
rpcError(
|
|
458
|
+
message?.id,
|
|
459
|
+
new JsonRpcError(
|
|
460
|
+
JSON_RPC_ERROR.OVERLOADED,
|
|
461
|
+
"App Server WebSocket receive queue is overloaded",
|
|
462
|
+
{ retry_after_ms: 100 },
|
|
463
|
+
),
|
|
464
|
+
),
|
|
465
|
+
);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const pending = Promise.resolve()
|
|
470
|
+
.then(() => context.server.receive(message))
|
|
471
|
+
.catch((error) => {
|
|
472
|
+
this.emit("connection-error", error);
|
|
473
|
+
void this._cleanup(context);
|
|
474
|
+
});
|
|
475
|
+
context.pending.add(pending);
|
|
476
|
+
pending.finally(() => context.pending.delete(pending));
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
_sendControl(context, message) {
|
|
480
|
+
void context.transport.send(message).catch((error) => {
|
|
481
|
+
this.emit("connection-error", error);
|
|
482
|
+
void this._cleanup(context);
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async _cleanup(context) {
|
|
487
|
+
if (context.cleanup) return context.cleanup;
|
|
488
|
+
context.closing = true;
|
|
489
|
+
this.connections.delete(context.socket);
|
|
490
|
+
context.cleanup = this._finishCleanup(context);
|
|
491
|
+
this.cleanups.add(context.cleanup);
|
|
492
|
+
void context.cleanup.then(
|
|
493
|
+
() => this.cleanups.delete(context.cleanup),
|
|
494
|
+
() => this.cleanups.delete(context.cleanup),
|
|
495
|
+
);
|
|
496
|
+
return context.cleanup;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async _finishCleanup(context) {
|
|
500
|
+
const settlement = Promise.allSettled([
|
|
501
|
+
context.transport.close(),
|
|
502
|
+
Promise.resolve().then(() => context.server.close?.()),
|
|
503
|
+
...context.pending,
|
|
504
|
+
]);
|
|
505
|
+
let timer;
|
|
506
|
+
const deadline = new Promise((resolve) => {
|
|
507
|
+
timer = setTimeout(resolve, this.cleanupTimeoutMs, "timeout");
|
|
508
|
+
timer.unref?.();
|
|
509
|
+
});
|
|
510
|
+
try {
|
|
511
|
+
if ((await Promise.race([settlement, deadline])) === "timeout") {
|
|
512
|
+
this.emit(
|
|
513
|
+
"connection-error",
|
|
514
|
+
new Error("App Server WebSocket cleanup deadline exceeded"),
|
|
515
|
+
);
|
|
516
|
+
context.socket.terminate?.();
|
|
517
|
+
}
|
|
518
|
+
} finally {
|
|
519
|
+
clearTimeout(timer);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async close() {
|
|
524
|
+
const contexts = [...this.connections.values()];
|
|
525
|
+
for (const context of contexts) {
|
|
526
|
+
try {
|
|
527
|
+
context.socket.close(1001, "App Server shutting down");
|
|
528
|
+
} catch {
|
|
529
|
+
context.socket.terminate?.();
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
await Promise.all(contexts.map((context) => this._cleanup(context)));
|
|
533
|
+
await Promise.allSettled([...this.cleanups]);
|
|
534
|
+
for (const context of contexts) {
|
|
535
|
+
if (context.socket.readyState !== 3) context.socket.terminate?.();
|
|
536
|
+
}
|
|
537
|
+
if (this.wss) {
|
|
538
|
+
await new Promise((resolve) => this.wss.close(() => resolve()));
|
|
539
|
+
this.wss = null;
|
|
540
|
+
}
|
|
541
|
+
if (this.httpServer) {
|
|
542
|
+
await new Promise((resolve) => this.httpServer.close(() => resolve()));
|
|
543
|
+
this.httpServer = null;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
@@ -53,6 +53,10 @@ const VSCODE_COMMANDS = [
|
|
|
53
53
|
"chainlesschain.sessions.workbench",
|
|
54
54
|
"chainlesschain.automation.center",
|
|
55
55
|
"chainlesschain.remote.control",
|
|
56
|
+
"chainlesschain.appServerPilot.status",
|
|
57
|
+
"chainlesschain.appServerPilot.startThread",
|
|
58
|
+
"chainlesschain.appServerPilot.startTurn",
|
|
59
|
+
"chainlesschain.appServerPilot.interrupt",
|
|
56
60
|
"chainlesschain.usage.show",
|
|
57
61
|
"chainlesschain.plugins.manage",
|
|
58
62
|
"chainlesschain.worktree.tasks",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { describe, it, expect } from "vitest";
|
|
15
15
|
import { createRequire } from "module";
|
|
16
|
+
import { readFileSync } from "node:fs";
|
|
16
17
|
|
|
17
18
|
const require = createRequire(import.meta.url);
|
|
18
19
|
const {
|
|
@@ -24,6 +25,7 @@ const {
|
|
|
24
25
|
wrapLegacyMessage,
|
|
25
26
|
validateCodingAgentEvent,
|
|
26
27
|
mapLegacyType,
|
|
28
|
+
projectAgentStreamMessage,
|
|
27
29
|
} = require("../coding-agent-events.cjs");
|
|
28
30
|
|
|
29
31
|
const VALID_TYPE = Object.values(CODING_AGENT_EVENT_TYPES)[0];
|
|
@@ -47,6 +49,40 @@ describe("mapLegacyType", () => {
|
|
|
47
49
|
});
|
|
48
50
|
});
|
|
49
51
|
|
|
52
|
+
describe("projectAgentStreamMessage", () => {
|
|
53
|
+
it("projects the shared canonical approval fixture without losing binding", () => {
|
|
54
|
+
const [request, resolved] = readFileSync(
|
|
55
|
+
new URL(
|
|
56
|
+
"../../../../agent-sdk/__fixtures__/protocol/interaction.ndjson",
|
|
57
|
+
import.meta.url,
|
|
58
|
+
),
|
|
59
|
+
"utf8",
|
|
60
|
+
)
|
|
61
|
+
.split(/\r?\n/u)
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.slice(0, 2)
|
|
64
|
+
.map((line) => JSON.parse(line));
|
|
65
|
+
|
|
66
|
+
expect(projectAgentStreamMessage(request)).toEqual({
|
|
67
|
+
type: CODING_AGENT_EVENT_TYPES.APPROVAL_REQUESTED,
|
|
68
|
+
payload: request,
|
|
69
|
+
});
|
|
70
|
+
expect(projectAgentStreamMessage(request).payload.binding).toBe(
|
|
71
|
+
"ab_0123456789abcdef0123456789abcdef",
|
|
72
|
+
);
|
|
73
|
+
expect(projectAgentStreamMessage(resolved)).toEqual({
|
|
74
|
+
type: CODING_AGENT_EVENT_TYPES.APPROVAL_GRANTED,
|
|
75
|
+
payload: resolved,
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("keeps unknown future stream events outside the semantic mapper", () => {
|
|
80
|
+
expect(
|
|
81
|
+
projectAgentStreamMessage({ type: "future_event_v9", value: true }),
|
|
82
|
+
).toBe(null);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
50
86
|
describe("CodingAgentSequenceTracker", () => {
|
|
51
87
|
it("increments strictly per requestId", () => {
|
|
52
88
|
const t = newTracker();
|
|
@@ -203,6 +203,87 @@ const LEGACY_TO_UNIFIED_TYPE = Object.freeze({
|
|
|
203
203
|
error: CODING_AGENT_EVENT_TYPES.ERROR,
|
|
204
204
|
});
|
|
205
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Project the canonical `cc agent --output-format stream-json` events that
|
|
208
|
+
* have a lossless Desktop Coding Agent equivalent. This is a semantic mapping
|
|
209
|
+
* of implemented UI states, not a second discriminator inventory: events with
|
|
210
|
+
* no equivalent return null and remain available to the raw transport.
|
|
211
|
+
*/
|
|
212
|
+
function projectAgentStreamMessage(message) {
|
|
213
|
+
if (!message || typeof message !== "object" || Array.isArray(message)) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let type = null;
|
|
218
|
+
const payload = { ...message };
|
|
219
|
+
switch (message.type) {
|
|
220
|
+
case "system":
|
|
221
|
+
if (message.subtype !== "init") return null;
|
|
222
|
+
type = CODING_AGENT_EVENT_TYPES.SESSION_STARTED;
|
|
223
|
+
payload.record = {
|
|
224
|
+
provider: message.provider || null,
|
|
225
|
+
model: message.model || null,
|
|
226
|
+
};
|
|
227
|
+
break;
|
|
228
|
+
case "stream_event": {
|
|
229
|
+
const delta = message.event?.delta;
|
|
230
|
+
if (delta?.type === "text_delta" && typeof delta.text === "string") {
|
|
231
|
+
type = CODING_AGENT_EVENT_TYPES.ASSISTANT_DELTA;
|
|
232
|
+
payload.content = delta.text;
|
|
233
|
+
} else if (
|
|
234
|
+
delta?.type === "thinking_delta" &&
|
|
235
|
+
typeof delta.thinking === "string"
|
|
236
|
+
) {
|
|
237
|
+
type = CODING_AGENT_EVENT_TYPES.ASSISTANT_THOUGHT_SUMMARY;
|
|
238
|
+
payload.content = delta.thinking;
|
|
239
|
+
} else {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
case "tool_use":
|
|
245
|
+
type = CODING_AGENT_EVENT_TYPES.TOOL_CALL_STARTED;
|
|
246
|
+
payload.toolName = message.tool;
|
|
247
|
+
break;
|
|
248
|
+
case "tool_result":
|
|
249
|
+
type =
|
|
250
|
+
message.is_error === true
|
|
251
|
+
? CODING_AGENT_EVENT_TYPES.TOOL_CALL_FAILED
|
|
252
|
+
: CODING_AGENT_EVENT_TYPES.TOOL_CALL_COMPLETED;
|
|
253
|
+
payload.toolName = message.tool;
|
|
254
|
+
break;
|
|
255
|
+
case "approval_request":
|
|
256
|
+
type = CODING_AGENT_EVENT_TYPES.APPROVAL_REQUESTED;
|
|
257
|
+
break;
|
|
258
|
+
case "approval_resolved":
|
|
259
|
+
type =
|
|
260
|
+
message.approved === true
|
|
261
|
+
? CODING_AGENT_EVENT_TYPES.APPROVAL_GRANTED
|
|
262
|
+
: CODING_AGENT_EVENT_TYPES.APPROVAL_DENIED;
|
|
263
|
+
break;
|
|
264
|
+
case "plan_update":
|
|
265
|
+
type = CODING_AGENT_EVENT_TYPES.PLAN_UPDATED;
|
|
266
|
+
break;
|
|
267
|
+
case "result":
|
|
268
|
+
type =
|
|
269
|
+
message.subtype === "interrupted" || message.interrupted === true
|
|
270
|
+
? CODING_AGENT_EVENT_TYPES.SESSION_INTERRUPTED
|
|
271
|
+
: CODING_AGENT_EVENT_TYPES.ASSISTANT_FINAL;
|
|
272
|
+
payload.content = message.error || message.result || null;
|
|
273
|
+
payload.isError = message.is_error === true;
|
|
274
|
+
break;
|
|
275
|
+
case "session_error":
|
|
276
|
+
type = CODING_AGENT_EVENT_TYPES.ERROR;
|
|
277
|
+
payload.message =
|
|
278
|
+
message.error || message.message || "Agent session failed";
|
|
279
|
+
break;
|
|
280
|
+
default:
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return { type, payload };
|
|
285
|
+
}
|
|
286
|
+
|
|
206
287
|
/**
|
|
207
288
|
* Map a legacy kebab-case type into the canonical dot-case type.
|
|
208
289
|
* Returns the original input if no mapping exists, so unknown types fall
|
|
@@ -414,4 +495,5 @@ module.exports = {
|
|
|
414
495
|
wrapLegacyMessage,
|
|
415
496
|
validateCodingAgentEvent,
|
|
416
497
|
mapLegacyType,
|
|
498
|
+
projectAgentStreamMessage,
|
|
417
499
|
};
|