@ganglion/xacpx-relay 0.11.3 → 0.12.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/dist/cli.js +550 -23
- package/dist/gateway/instance-gateway.d.ts +7 -1
- package/dist/gateway/web-gateway.d.ts +56 -1
- package/dist/gateway/web-inbound.d.ts +10 -8
- package/dist/relay-web/assets/{DashboardView-Dtb2Glmt.css → DashboardView-BNwcKQ6u.css} +1 -1
- package/dist/relay-web/assets/DashboardView-CM38P675.js +436 -0
- package/dist/relay-web/assets/{InviteView-DtidlMrD.js → InviteView-C3kg9Vwr.js} +1 -1
- package/dist/relay-web/assets/{LoginView-CNkNRebg.js → LoginView-DoM2bMxD.js} +1 -1
- package/dist/relay-web/assets/{SettingsView-DCYGtpUv.js → SettingsView-D2ejxeDR.js} +1 -1
- package/dist/relay-web/assets/{abnfDiagram-VRR7QNED-Bd29dhCa.js → abnfDiagram-VRR7QNED-deRHrXse.js} +1 -1
- package/dist/relay-web/assets/{arc-CrQfTVtY.js → arc-DN18fNDk.js} +1 -1
- package/dist/relay-web/assets/{architectureDiagram-ZJ3FMSHR-CLNuP_U4.js → architectureDiagram-ZJ3FMSHR-BkPUXZS-.js} +1 -1
- package/dist/relay-web/assets/{blockDiagram-677ZJIJ3-CGqit6bA.js → blockDiagram-677ZJIJ3-B5TMW0sV.js} +1 -1
- package/dist/relay-web/assets/{c4Diagram-LMCZKHZV-DD7o_s-W.js → c4Diagram-LMCZKHZV-DaqXZK5X.js} +1 -1
- package/dist/relay-web/assets/channel-DUBJdIk3.js +1 -0
- package/dist/relay-web/assets/{chunk-2Q5K7J3B-DHLIOuAq.js → chunk-2Q5K7J3B-CulRrf5Q.js} +1 -1
- package/dist/relay-web/assets/{chunk-32BRIVSS-BENzymJv.js → chunk-32BRIVSS-Bs0K3Abr.js} +1 -1
- package/dist/relay-web/assets/{chunk-5VM5RSS4-wPvLHeXr.js → chunk-5VM5RSS4-DLC3RAol.js} +1 -1
- package/dist/relay-web/assets/{chunk-EX3LRPZG-CE48BnPr.js → chunk-EX3LRPZG-BQztEinp.js} +1 -1
- package/dist/relay-web/assets/{chunk-JWPE2WC7-xqfxYXvP.js → chunk-JWPE2WC7-BeYzb5GM.js} +1 -1
- package/dist/relay-web/assets/{chunk-MOJQB5TN-B7xWKcW9.js → chunk-MOJQB5TN-wdl6yjvQ.js} +1 -1
- package/dist/relay-web/assets/{chunk-RYQCIY6F-DDBvROhN.js → chunk-RYQCIY6F-CfiIg3aE.js} +1 -1
- package/dist/relay-web/assets/{chunk-V7JOEXUC-Bmzqw51I.js → chunk-V7JOEXUC-DDuTsOmE.js} +1 -1
- package/dist/relay-web/assets/{chunk-VR4S4FIN-BbZAqPQo.js → chunk-VR4S4FIN-Blf49feW.js} +1 -1
- package/dist/relay-web/assets/{chunk-XXDRQBXY-DKrO8Usy.js → chunk-XXDRQBXY-BWoej7D5.js} +1 -1
- package/dist/relay-web/assets/classDiagram-OUVF2IWQ-C07wXHb7.js +1 -0
- package/dist/relay-web/assets/classDiagram-v2-EOCWNBFH-C07wXHb7.js +1 -0
- package/dist/relay-web/assets/{cose-bilkent-JH36ORCC-yBWTTk4O.js → cose-bilkent-JH36ORCC-Dw3-NQmG.js} +1 -1
- package/dist/relay-web/assets/{cynefin-VYW2F7L2-B-tHJ3Js.js → cynefin-VYW2F7L2-CCqas_tu.js} +1 -1
- package/dist/relay-web/assets/{cynefinDiagram-TSTJHNR4-DNw-0MKO.js → cynefinDiagram-TSTJHNR4-CUxgvNWK.js} +1 -1
- package/dist/relay-web/assets/{dagre-VKFMJZFB-DbOX-V0S.js → dagre-VKFMJZFB-DTufXiab.js} +1 -1
- package/dist/relay-web/assets/{diagram-FQU43EPY-NHSV-gLW.js → diagram-FQU43EPY-B4BMTPl8.js} +1 -1
- package/dist/relay-web/assets/{diagram-G47NLZAW-B5zpBk_f.js → diagram-G47NLZAW-BGE6VzZs.js} +1 -1
- package/dist/relay-web/assets/{diagram-NH7WQ7WH-Coan_Ptw.js → diagram-NH7WQ7WH-DAxE22CH.js} +1 -1
- package/dist/relay-web/assets/{diagram-OA4YK3LP-ZYEaYDqj.js → diagram-OA4YK3LP-CyfVqStt.js} +1 -1
- package/dist/relay-web/assets/{diagram-WEI45ONY-DDvCyduS.js → diagram-WEI45ONY-DxzIDJtH.js} +1 -1
- package/dist/relay-web/assets/{ebnfDiagram-CCIWWBDH-BJ__ou6u.js → ebnfDiagram-CCIWWBDH-Dwe8jOTg.js} +1 -1
- package/dist/relay-web/assets/{erDiagram-Q63AITRT-BmCBLrxn.js → erDiagram-Q63AITRT-BjkjUdTK.js} +1 -1
- package/dist/relay-web/assets/{flowDiagram-23GEKE2U-C7gtTBpm.js → flowDiagram-23GEKE2U-DLZaZxoR.js} +1 -1
- package/dist/relay-web/assets/{ganttDiagram-NO4QXBWP-DOCLZLky.js → ganttDiagram-NO4QXBWP-Cugmec_D.js} +1 -1
- package/dist/relay-web/assets/{ghostty-web-DJNNWGKw.js → ghostty-web-Btdylsvz.js} +1 -1
- package/dist/relay-web/assets/{gitGraphDiagram-IHSO6WYX-CmHqnB47.js → gitGraphDiagram-IHSO6WYX-CvtXdEWO.js} +1 -1
- package/dist/relay-web/assets/{index-B29mFOXU.js → index-D52ntTFm.js} +18 -18
- package/dist/relay-web/assets/{infoDiagram-FWYZ7A6U-BqeSGvj4.js → infoDiagram-FWYZ7A6U-CyisQOMU.js} +1 -1
- package/dist/relay-web/assets/{ishikawaDiagram-FXEZZL3T-D1teo9mW.js → ishikawaDiagram-FXEZZL3T-AdWvcsaG.js} +1 -1
- package/dist/relay-web/assets/{journeyDiagram-5HDEW3XC-C0PwKyKj.js → journeyDiagram-5HDEW3XC-BPvobm1L.js} +1 -1
- package/dist/relay-web/assets/{kanban-definition-HUTT4EX6-C1PBukK2.js → kanban-definition-HUTT4EX6-I5jq5AhB.js} +1 -1
- package/dist/relay-web/assets/{linear-CmAS5Fla.js → linear-z4GCDjJV.js} +1 -1
- package/dist/relay-web/assets/{mermaid.core-B17Q-J1t.js → mermaid.core-BdsVh7Pj.js} +4 -4
- package/dist/relay-web/assets/{mindmap-definition-LN4V7U3C-DYxYq6Un.js → mindmap-definition-LN4V7U3C-DzGr1rLu.js} +1 -1
- package/dist/relay-web/assets/{pegDiagram-2B236MQR-BlCDSiXA.js → pegDiagram-2B236MQR-Bf60O0vf.js} +1 -1
- package/dist/relay-web/assets/{pieDiagram-ENE6RG2P-BCINN16i.js → pieDiagram-ENE6RG2P-3JHpstBi.js} +1 -1
- package/dist/relay-web/assets/{quadrantDiagram-ABIIQ3AL-C9b5gZyw.js → quadrantDiagram-ABIIQ3AL-Bn8A4z_W.js} +1 -1
- package/dist/relay-web/assets/{railroadDiagram-RFXS5EU6-BxIP6BRa.js → railroadDiagram-RFXS5EU6-Cm9CSksD.js} +1 -1
- package/dist/relay-web/assets/{requirementDiagram-TGXJPOKE-D-SMDKif.js → requirementDiagram-TGXJPOKE-Rz0DyU_-.js} +1 -1
- package/dist/relay-web/assets/{sankeyDiagram-HTMAVEWB-rTjFCUNr.js → sankeyDiagram-HTMAVEWB-Cgb-wJlV.js} +1 -1
- package/dist/relay-web/assets/{sequenceDiagram-DBY2YBRQ-BsqP7y8O.js → sequenceDiagram-DBY2YBRQ-C7FgOmV3.js} +1 -1
- package/dist/relay-web/assets/{sizeCapture-X5ZJPWSS-CW369UlW.js → sizeCapture-X5ZJPWSS-CP1rDXXR.js} +1 -1
- package/dist/relay-web/assets/{stateDiagram-2N3HPSRC-B9arh27E.js → stateDiagram-2N3HPSRC-Di5pHgGQ.js} +1 -1
- package/dist/relay-web/assets/stateDiagram-v2-6OUMAXLB-BPeNqnaa.js +1 -0
- package/dist/relay-web/assets/{swimlanes-5IMT3BWC-ClJcyhhP.js → swimlanes-5IMT3BWC-BJoJKh0U.js} +2 -2
- package/dist/relay-web/assets/swimlanesDiagram-G3AALYLV-Cm9UEn_b.js +8 -0
- package/dist/relay-web/assets/{theme-1IR1O1bT.js → theme-31dEzsN7.js} +1 -1
- package/dist/relay-web/assets/{timeline-definition-FHXFAJF6-DAJSZFfu.js → timeline-definition-FHXFAJF6-CxE_BEJ7.js} +1 -1
- package/dist/relay-web/assets/{vennDiagram-L72KCM5P-Dpab5g2u.js → vennDiagram-L72KCM5P-sguHqvWG.js} +1 -1
- package/dist/relay-web/assets/{wardleyDiagram-EHGQE667-CqO8MTvZ.js → wardleyDiagram-EHGQE667-CEuHfxX6.js} +1 -1
- package/dist/relay-web/assets/{xychartDiagram-FW5EYKEG-CDpFaAWf.js → xychartDiagram-FW5EYKEG-CLljoDKz.js} +1 -1
- package/dist/relay-web/index.html +1 -1
- package/dist/relay-web/sw.js +1 -1
- package/dist/stores/instances.d.ts +10 -1
- package/package.json +2 -2
- package/dist/relay-web/assets/DashboardView-BENAQLdv.js +0 -436
- package/dist/relay-web/assets/channel-BDCHXdrq.js +0 -1
- package/dist/relay-web/assets/classDiagram-OUVF2IWQ-w2002tim.js +0 -1
- package/dist/relay-web/assets/classDiagram-v2-EOCWNBFH-w2002tim.js +0 -1
- package/dist/relay-web/assets/stateDiagram-v2-6OUMAXLB-nhU-bjvr.js +0 -1
- package/dist/relay-web/assets/swimlanesDiagram-G3AALYLV-CHH67zRv.js +0 -8
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
4
4
|
|
|
5
5
|
// packages/relay/src/cli.ts
|
|
6
|
-
import { randomUUID as
|
|
6
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
7
7
|
import { existsSync } from "node:fs";
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
9
|
import { dirname as dirname3, join as join2, resolve } from "node:path";
|
|
@@ -140,6 +140,7 @@ function initSchema(db) {
|
|
|
140
140
|
name TEXT NOT NULL,
|
|
141
141
|
credential_hash TEXT NOT NULL,
|
|
142
142
|
core_version TEXT,
|
|
143
|
+
capabilities_json TEXT,
|
|
143
144
|
last_seen_at TEXT,
|
|
144
145
|
created_at TEXT NOT NULL
|
|
145
146
|
);
|
|
@@ -181,6 +182,10 @@ function initSchema(db) {
|
|
|
181
182
|
if (!messageCols.some((c) => c.name === "prompt_request_id")) {
|
|
182
183
|
db.exec("ALTER TABLE messages ADD COLUMN prompt_request_id TEXT");
|
|
183
184
|
}
|
|
185
|
+
const instanceCols = db.all("PRAGMA table_info(instances)");
|
|
186
|
+
if (!instanceCols.some((c) => c.name === "capabilities_json")) {
|
|
187
|
+
db.exec("ALTER TABLE instances ADD COLUMN capabilities_json TEXT");
|
|
188
|
+
}
|
|
184
189
|
db.exec(`
|
|
185
190
|
CREATE INDEX IF NOT EXISTS idx_messages_origin_queue ON messages (instance_id, session_alias, origin_queue_item_id);
|
|
186
191
|
CREATE INDEX IF NOT EXISTS idx_messages_prompt_request ON messages (prompt_request_id);
|
|
@@ -452,16 +457,31 @@ class InstanceStore {
|
|
|
452
457
|
return null;
|
|
453
458
|
return toInstanceRow(row);
|
|
454
459
|
}
|
|
455
|
-
touch(instanceId, coreVersion) {
|
|
460
|
+
touch(instanceId, coreVersion, capabilities) {
|
|
461
|
+
const nowIso = this.now().toISOString();
|
|
462
|
+
if (capabilities !== undefined && coreVersion !== undefined) {
|
|
463
|
+
this.db.run("UPDATE instances SET last_seen_at = ?, core_version = ?, capabilities_json = ? WHERE id = ?", [nowIso, coreVersion, JSON.stringify(capabilities), instanceId]);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (capabilities !== undefined) {
|
|
467
|
+
this.db.run("UPDATE instances SET last_seen_at = ?, capabilities_json = ? WHERE id = ?", [nowIso, JSON.stringify(capabilities), instanceId]);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
456
470
|
if (coreVersion !== undefined) {
|
|
457
471
|
this.db.run("UPDATE instances SET last_seen_at = ?, core_version = ? WHERE id = ?", [
|
|
458
|
-
|
|
472
|
+
nowIso,
|
|
459
473
|
coreVersion,
|
|
460
474
|
instanceId
|
|
461
475
|
]);
|
|
462
476
|
return;
|
|
463
477
|
}
|
|
464
|
-
this.db.run("UPDATE instances SET last_seen_at = ? WHERE id = ?", [
|
|
478
|
+
this.db.run("UPDATE instances SET last_seen_at = ? WHERE id = ?", [nowIso, instanceId]);
|
|
479
|
+
}
|
|
480
|
+
setCapabilities(instanceId, capabilities) {
|
|
481
|
+
this.db.run("UPDATE instances SET capabilities_json = ? WHERE id = ?", [
|
|
482
|
+
JSON.stringify(capabilities),
|
|
483
|
+
instanceId
|
|
484
|
+
]);
|
|
465
485
|
}
|
|
466
486
|
listByAccount(accountId) {
|
|
467
487
|
return this.db.all("SELECT * FROM instances WHERE account_id = ? ORDER BY created_at", [accountId]).map(toInstanceRow);
|
|
@@ -489,12 +509,23 @@ class InstanceStore {
|
|
|
489
509
|
return row?.n ?? 0;
|
|
490
510
|
}
|
|
491
511
|
}
|
|
512
|
+
function parseCapabilitiesJson(raw) {
|
|
513
|
+
if (raw == null || raw === "")
|
|
514
|
+
return [];
|
|
515
|
+
try {
|
|
516
|
+
const parsed = JSON.parse(raw);
|
|
517
|
+
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : [];
|
|
518
|
+
} catch {
|
|
519
|
+
return [];
|
|
520
|
+
}
|
|
521
|
+
}
|
|
492
522
|
function toInstanceRow(row) {
|
|
493
523
|
return {
|
|
494
524
|
id: row.id,
|
|
495
525
|
accountId: row.account_id,
|
|
496
526
|
name: row.name,
|
|
497
527
|
coreVersion: row.core_version,
|
|
528
|
+
capabilities: parseCapabilitiesJson(row.capabilities_json),
|
|
498
529
|
lastSeenAt: row.last_seen_at,
|
|
499
530
|
createdAt: row.created_at
|
|
500
531
|
};
|
|
@@ -650,9 +681,11 @@ class RecoveryReceiptStore {
|
|
|
650
681
|
import {
|
|
651
682
|
MSG,
|
|
652
683
|
RELAY_PROTOCOL_VERSION,
|
|
684
|
+
TERMINAL_HUB_REQUEST_TIMEOUT_MS,
|
|
653
685
|
decodeEnvelope,
|
|
654
686
|
encodeEnvelope,
|
|
655
|
-
errorPayload
|
|
687
|
+
errorPayload,
|
|
688
|
+
normalizeCapabilities
|
|
656
689
|
} from "@ganglion/xacpx-relay-protocol";
|
|
657
690
|
|
|
658
691
|
// packages/relay/src/logging.ts
|
|
@@ -736,6 +769,7 @@ function startHeartbeat(socket, intervalMs = HEARTBEAT_INTERVAL_MS, maxMissed =
|
|
|
736
769
|
// packages/relay/src/gateway/instance-gateway.ts
|
|
737
770
|
var DEFAULT_REQUEST_TIMEOUT_MS = 120000;
|
|
738
771
|
var REQUEST_RESPONSE_RESERVE_MS = 15000;
|
|
772
|
+
var TERMINAL_REQUEST_TIMEOUT_MS = TERMINAL_HUB_REQUEST_TIMEOUT_MS;
|
|
739
773
|
|
|
740
774
|
class InstanceGateway {
|
|
741
775
|
deps;
|
|
@@ -873,7 +907,7 @@ class InstanceGateway {
|
|
|
873
907
|
result = redeemed;
|
|
874
908
|
}
|
|
875
909
|
respond({ instanceId: result.instanceId, credential: result.credential });
|
|
876
|
-
this.deps.instances.touch(result.instanceId);
|
|
910
|
+
this.deps.instances.touch(result.instanceId, payload?.coreVersion, normalizeCapabilities(payload?.capabilities));
|
|
877
911
|
return { instanceId: result.instanceId, accountId: result.accountId };
|
|
878
912
|
}
|
|
879
913
|
if (envelope.type === MSG.instanceAuth) {
|
|
@@ -886,7 +920,7 @@ class InstanceGateway {
|
|
|
886
920
|
return null;
|
|
887
921
|
}
|
|
888
922
|
respond({ ok: true });
|
|
889
|
-
this.deps.instances.touch(instance.id, payload?.coreVersion);
|
|
923
|
+
this.deps.instances.touch(instance.id, payload?.coreVersion, normalizeCapabilities(payload?.capabilities));
|
|
890
924
|
return { instanceId: instance.id, accountId: instance.accountId };
|
|
891
925
|
}
|
|
892
926
|
this.logger.info("relay.instance.handshake_failed", "handshake rejected", { reason: "unknown-message-type" });
|
|
@@ -912,13 +946,13 @@ class InstanceGateway {
|
|
|
912
946
|
this.logger.debug("relay.instance.disconnect_failed", "closing revoked instance socket failed", { instanceId, error: String(err) });
|
|
913
947
|
}
|
|
914
948
|
}
|
|
915
|
-
async sendRequest(instanceId, type, payload) {
|
|
949
|
+
async sendRequest(instanceId, type, payload, options) {
|
|
916
950
|
const connection = this.connections.get(instanceId);
|
|
917
951
|
if (!connection) {
|
|
918
952
|
throw new Error("instance-offline");
|
|
919
953
|
}
|
|
920
954
|
const id = `relay-${++this.seq}`;
|
|
921
|
-
const timeoutMs = this.deps.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
955
|
+
const timeoutMs = options?.timeoutMs ?? this.deps.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
922
956
|
const requestBudgetMs = Math.max(timeoutMs - REQUEST_RESPONSE_RESERVE_MS, 1);
|
|
923
957
|
const requestDeadlineAt = Date.now() + requestBudgetMs;
|
|
924
958
|
return await new Promise((resolve, reject) => {
|
|
@@ -950,14 +984,24 @@ class InstanceGateway {
|
|
|
950
984
|
}
|
|
951
985
|
|
|
952
986
|
// packages/relay/src/gateway/web-gateway.ts
|
|
987
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
953
988
|
import { encodeEnvelope as encodeEnvelope2, webEventEnvelope } from "@ganglion/xacpx-relay-protocol";
|
|
954
989
|
var WS_OPEN = 1;
|
|
955
990
|
var BACKPRESSURE_MAX = 4 * 1024 * 1024;
|
|
991
|
+
function terminalKey(instanceId, terminalId) {
|
|
992
|
+
return `${instanceId}\x00${terminalId}`;
|
|
993
|
+
}
|
|
956
994
|
|
|
957
995
|
class WebGateway {
|
|
958
996
|
options;
|
|
959
997
|
byAccount = new Map;
|
|
960
998
|
subscriptions = new Map;
|
|
999
|
+
viewerBySocket = new Map;
|
|
1000
|
+
accountBySocket = new Map;
|
|
1001
|
+
attachmentsBySocket = new Map;
|
|
1002
|
+
socketByAttachment = new Map;
|
|
1003
|
+
bindingByAttachment = new Map;
|
|
1004
|
+
attachmentsByTerminal = new Map;
|
|
961
1005
|
constructor(options = {}) {
|
|
962
1006
|
this.options = options;
|
|
963
1007
|
}
|
|
@@ -965,19 +1009,105 @@ class WebGateway {
|
|
|
965
1009
|
const set = this.byAccount.get(accountId) ?? new Set;
|
|
966
1010
|
set.add(socket);
|
|
967
1011
|
this.byAccount.set(accountId, set);
|
|
1012
|
+
const viewerId = randomUUID3();
|
|
1013
|
+
this.viewerBySocket.set(socket, viewerId);
|
|
1014
|
+
this.accountBySocket.set(socket, accountId);
|
|
1015
|
+
this.attachmentsBySocket.set(socket, new Set);
|
|
968
1016
|
this.options.logger?.debug("relay.web.connected", "web client connected", { accountId });
|
|
969
1017
|
startHeartbeat(socket, this.options.heartbeatIntervalMs, undefined, this.options.logger);
|
|
970
1018
|
socket.on("close", () => {
|
|
1019
|
+
this.clearSocketAttachments(socket);
|
|
971
1020
|
set.delete(socket);
|
|
972
1021
|
this.subscriptions.delete(socket);
|
|
1022
|
+
this.viewerBySocket.delete(socket);
|
|
1023
|
+
this.accountBySocket.delete(socket);
|
|
1024
|
+
this.attachmentsBySocket.delete(socket);
|
|
973
1025
|
if (set.size === 0)
|
|
974
1026
|
this.byAccount.delete(accountId);
|
|
975
1027
|
this.options.logger?.debug("relay.web.disconnected", "web client disconnected", { accountId });
|
|
976
1028
|
});
|
|
1029
|
+
return viewerId;
|
|
1030
|
+
}
|
|
1031
|
+
getViewerId(socket) {
|
|
1032
|
+
return this.viewerBySocket.get(socket);
|
|
977
1033
|
}
|
|
978
1034
|
setSubscription(socket, instanceIds) {
|
|
979
1035
|
this.subscriptions.set(socket, new Set(instanceIds));
|
|
980
1036
|
}
|
|
1037
|
+
bindAttachment(input) {
|
|
1038
|
+
const viewerId = this.viewerBySocket.get(input.socket);
|
|
1039
|
+
if (!viewerId) {
|
|
1040
|
+
throw new Error("socket has no viewerId; register() before bindAttachment");
|
|
1041
|
+
}
|
|
1042
|
+
this.unbindAttachment(input.attachmentId, { notifyDetach: false });
|
|
1043
|
+
const binding = {
|
|
1044
|
+
attachmentId: input.attachmentId,
|
|
1045
|
+
viewerId,
|
|
1046
|
+
terminalId: input.terminalId,
|
|
1047
|
+
instanceId: input.instanceId,
|
|
1048
|
+
socket: input.socket
|
|
1049
|
+
};
|
|
1050
|
+
this.bindingByAttachment.set(input.attachmentId, binding);
|
|
1051
|
+
this.socketByAttachment.set(input.attachmentId, input.socket);
|
|
1052
|
+
const sockSet = this.attachmentsBySocket.get(input.socket) ?? new Set;
|
|
1053
|
+
sockSet.add(input.attachmentId);
|
|
1054
|
+
this.attachmentsBySocket.set(input.socket, sockSet);
|
|
1055
|
+
const tKey = terminalKey(input.instanceId, input.terminalId);
|
|
1056
|
+
const termSet = this.attachmentsByTerminal.get(tKey) ?? new Set;
|
|
1057
|
+
termSet.add(input.attachmentId);
|
|
1058
|
+
this.attachmentsByTerminal.set(tKey, termSet);
|
|
1059
|
+
}
|
|
1060
|
+
unbindAttachment(attachmentId, opts = {}) {
|
|
1061
|
+
const binding = this.bindingByAttachment.get(attachmentId);
|
|
1062
|
+
if (!binding)
|
|
1063
|
+
return;
|
|
1064
|
+
this.bindingByAttachment.delete(attachmentId);
|
|
1065
|
+
this.socketByAttachment.delete(attachmentId);
|
|
1066
|
+
this.attachmentsBySocket.get(binding.socket)?.delete(attachmentId);
|
|
1067
|
+
const tKey = terminalKey(binding.instanceId, binding.terminalId);
|
|
1068
|
+
const termSet = this.attachmentsByTerminal.get(tKey);
|
|
1069
|
+
if (termSet) {
|
|
1070
|
+
termSet.delete(attachmentId);
|
|
1071
|
+
if (termSet.size === 0)
|
|
1072
|
+
this.attachmentsByTerminal.delete(tKey);
|
|
1073
|
+
}
|
|
1074
|
+
if (opts.notifyDetach !== false) {
|
|
1075
|
+
this.options.onAttachmentDetached?.({
|
|
1076
|
+
instanceId: binding.instanceId,
|
|
1077
|
+
attachmentId: binding.attachmentId,
|
|
1078
|
+
viewerId: binding.viewerId
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
return binding;
|
|
1082
|
+
}
|
|
1083
|
+
getAttachmentBinding(attachmentId) {
|
|
1084
|
+
return this.bindingByAttachment.get(attachmentId);
|
|
1085
|
+
}
|
|
1086
|
+
socketOwnsAttachment(socket, attachmentId) {
|
|
1087
|
+
return this.socketByAttachment.get(attachmentId) === socket;
|
|
1088
|
+
}
|
|
1089
|
+
sendToAttachment(viewerId, attachmentId, event) {
|
|
1090
|
+
const binding = this.bindingByAttachment.get(attachmentId);
|
|
1091
|
+
if (!binding)
|
|
1092
|
+
return false;
|
|
1093
|
+
if (binding.viewerId !== viewerId)
|
|
1094
|
+
return false;
|
|
1095
|
+
if (this.socketByAttachment.get(attachmentId) !== binding.socket)
|
|
1096
|
+
return false;
|
|
1097
|
+
return this.send(binding.socket, event);
|
|
1098
|
+
}
|
|
1099
|
+
fanoutTerminalExit(instanceId, terminalId, event) {
|
|
1100
|
+
const ids = this.attachmentsByTerminal.get(terminalKey(instanceId, terminalId));
|
|
1101
|
+
if (!ids)
|
|
1102
|
+
return;
|
|
1103
|
+
for (const attachmentId of [...ids]) {
|
|
1104
|
+
const binding = this.bindingByAttachment.get(attachmentId);
|
|
1105
|
+
if (!binding)
|
|
1106
|
+
continue;
|
|
1107
|
+
this.send(binding.socket, event);
|
|
1108
|
+
this.unbindAttachment(attachmentId, { notifyDetach: false });
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
981
1111
|
send(socket, event) {
|
|
982
1112
|
return this.sendEncoded(socket, encodeEnvelope2(webEventEnvelope(event)));
|
|
983
1113
|
}
|
|
@@ -989,6 +1119,7 @@ class WebGateway {
|
|
|
989
1119
|
...accountId ? { accountId } : {},
|
|
990
1120
|
bufferedAmount: socket.bufferedAmount
|
|
991
1121
|
});
|
|
1122
|
+
this.clearSocketAttachments(socket);
|
|
992
1123
|
try {
|
|
993
1124
|
socket.terminate?.();
|
|
994
1125
|
} catch {}
|
|
@@ -996,11 +1127,17 @@ class WebGateway {
|
|
|
996
1127
|
}
|
|
997
1128
|
try {
|
|
998
1129
|
socket.send(data);
|
|
999
|
-
return true;
|
|
1000
1130
|
} catch (err) {
|
|
1001
1131
|
this.options.logger?.error("relay.web.broadcast_failed", "web event send failed", { error: String(err) });
|
|
1002
1132
|
return false;
|
|
1003
1133
|
}
|
|
1134
|
+
return true;
|
|
1135
|
+
}
|
|
1136
|
+
clearSocketAttachments(socket) {
|
|
1137
|
+
const ids = [...this.attachmentsBySocket.get(socket) ?? []];
|
|
1138
|
+
for (const attachmentId of ids) {
|
|
1139
|
+
this.unbindAttachment(attachmentId, { notifyDetach: true });
|
|
1140
|
+
}
|
|
1004
1141
|
}
|
|
1005
1142
|
broadcast(accountId, event) {
|
|
1006
1143
|
const set = this.byAccount.get(accountId);
|
|
@@ -1020,8 +1157,38 @@ class WebGateway {
|
|
|
1020
1157
|
}
|
|
1021
1158
|
|
|
1022
1159
|
// packages/relay/src/gateway/web-inbound.ts
|
|
1023
|
-
import {
|
|
1160
|
+
import {
|
|
1161
|
+
decodeEnvelope as decodeEnvelope2,
|
|
1162
|
+
isErrorPayload,
|
|
1163
|
+
MSG as MSG2,
|
|
1164
|
+
parseTerminalEventPayload,
|
|
1165
|
+
parseWebClientMessage
|
|
1166
|
+
} from "@ganglion/xacpx-relay-protocol";
|
|
1167
|
+
function fail(deps, socket, requestId, instanceId, code, message) {
|
|
1168
|
+
deps.webGateway.send(socket, {
|
|
1169
|
+
kind: "terminal-request-failed",
|
|
1170
|
+
requestId,
|
|
1171
|
+
instanceId,
|
|
1172
|
+
code,
|
|
1173
|
+
message
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
function mapConnectorError(err) {
|
|
1177
|
+
if (err instanceof Error) {
|
|
1178
|
+
if (err.message === "instance-offline" || err.message === "instance-reconnected") {
|
|
1179
|
+
return { code: "instance-offline", message: err.message };
|
|
1180
|
+
}
|
|
1181
|
+
if (err.message === "timeout") {
|
|
1182
|
+
return { code: "terminal-timeout", message: "terminal request timed out" };
|
|
1183
|
+
}
|
|
1184
|
+
return { code: "terminal-protocol-error", message: err.message };
|
|
1185
|
+
}
|
|
1186
|
+
return { code: "terminal-protocol-error", message: String(err) };
|
|
1187
|
+
}
|
|
1024
1188
|
function handleWebClientMessage(deps, accountId, socket, raw) {
|
|
1189
|
+
handleWebClientMessageAsync(deps, accountId, socket, raw);
|
|
1190
|
+
}
|
|
1191
|
+
async function handleWebClientMessageAsync(deps, accountId, socket, raw) {
|
|
1025
1192
|
const decoded = decodeEnvelope2(raw);
|
|
1026
1193
|
if (!decoded.ok)
|
|
1027
1194
|
return;
|
|
@@ -1033,26 +1200,373 @@ function handleWebClientMessage(deps, accountId, socket, raw) {
|
|
|
1033
1200
|
const instanceIds = [...new Set(msg.instanceIds)].filter((id) => ownedIds.has(id));
|
|
1034
1201
|
deps.webGateway.setSubscription(socket, instanceIds);
|
|
1035
1202
|
for (const instanceId of instanceIds) {
|
|
1036
|
-
deps.webGateway.send(socket, {
|
|
1203
|
+
deps.webGateway.send(socket, {
|
|
1204
|
+
kind: "state-snapshot",
|
|
1205
|
+
instanceId,
|
|
1206
|
+
...deps.stateSnapshot(instanceId)
|
|
1207
|
+
});
|
|
1037
1208
|
}
|
|
1038
1209
|
return;
|
|
1039
1210
|
}
|
|
1040
1211
|
if (!deps.instances.getOwned(msg.instanceId, accountId))
|
|
1041
1212
|
return;
|
|
1042
|
-
if (msg.kind === "terminal-
|
|
1043
|
-
deps
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1213
|
+
if (msg.kind === "terminal-open") {
|
|
1214
|
+
await handleTerminalOpen(deps, accountId, socket, msg);
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
if (msg.kind === "terminal-stream-start") {
|
|
1218
|
+
const viewerId = deps.webGateway.getViewerId(socket);
|
|
1219
|
+
if (!viewerId || !deps.webGateway.socketOwnsAttachment(socket, msg.attachmentId))
|
|
1220
|
+
return;
|
|
1221
|
+
deps.gateway.sendEvent(msg.instanceId, MSG2.terminalStreamStart, {
|
|
1222
|
+
attachmentId: msg.attachmentId,
|
|
1223
|
+
viewerId
|
|
1224
|
+
});
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
if (msg.kind === "terminal-take-control") {
|
|
1228
|
+
await handleTakeControl(deps, socket, msg);
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
if (msg.kind === "terminal-resync") {
|
|
1232
|
+
await handleResync(deps, socket, msg);
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
if (msg.kind === "terminal-terminate") {
|
|
1236
|
+
await handleTerminate(deps, socket, msg);
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
if (msg.kind === "terminal-input" && "attachmentId" in msg) {
|
|
1240
|
+
const viewerId = deps.webGateway.getViewerId(socket);
|
|
1241
|
+
if (!viewerId || !deps.webGateway.socketOwnsAttachment(socket, msg.attachmentId))
|
|
1242
|
+
return;
|
|
1243
|
+
deps.gateway.sendEvent(msg.instanceId, MSG2.terminalInput, {
|
|
1244
|
+
attachmentId: msg.attachmentId,
|
|
1245
|
+
generation: msg.generation,
|
|
1246
|
+
viewerId,
|
|
1247
|
+
dataBase64: msg.dataBase64
|
|
1248
|
+
});
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
if (msg.kind === "terminal-resize" && "attachmentId" in msg) {
|
|
1252
|
+
const viewerId = deps.webGateway.getViewerId(socket);
|
|
1253
|
+
if (!viewerId || !deps.webGateway.socketOwnsAttachment(socket, msg.attachmentId))
|
|
1254
|
+
return;
|
|
1255
|
+
deps.gateway.sendEvent(msg.instanceId, MSG2.terminalResize, {
|
|
1256
|
+
attachmentId: msg.attachmentId,
|
|
1257
|
+
generation: msg.generation,
|
|
1258
|
+
viewerId,
|
|
1259
|
+
cols: msg.cols,
|
|
1260
|
+
rows: msg.rows
|
|
1261
|
+
});
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
if (msg.kind === "terminal-heartbeat") {
|
|
1265
|
+
const viewerId = deps.webGateway.getViewerId(socket);
|
|
1266
|
+
if (!viewerId || !deps.webGateway.socketOwnsAttachment(socket, msg.attachmentId))
|
|
1267
|
+
return;
|
|
1268
|
+
deps.gateway.sendEvent(msg.instanceId, MSG2.terminalHeartbeat, {
|
|
1269
|
+
attachmentId: msg.attachmentId,
|
|
1270
|
+
viewerId
|
|
1271
|
+
});
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
if (msg.kind === "terminal-detach") {
|
|
1275
|
+
if (!deps.webGateway.socketOwnsAttachment(socket, msg.attachmentId))
|
|
1276
|
+
return;
|
|
1277
|
+
const binding = deps.webGateway.unbindAttachment(msg.attachmentId);
|
|
1278
|
+
if (binding) {
|
|
1279
|
+
deps.gateway.sendEvent(msg.instanceId, MSG2.terminalDetach, {
|
|
1280
|
+
attachmentId: msg.attachmentId,
|
|
1281
|
+
viewerId: binding.viewerId
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
if (msg.kind === "terminal-input" && "terminalId" in msg) {
|
|
1287
|
+
deps.gateway.sendEvent(msg.instanceId, MSG2.terminalInput, {
|
|
1288
|
+
terminalId: msg.terminalId,
|
|
1289
|
+
data: msg.data
|
|
1290
|
+
});
|
|
1291
|
+
} else if (msg.kind === "terminal-resize" && "terminalId" in msg) {
|
|
1292
|
+
deps.gateway.sendEvent(msg.instanceId, MSG2.terminalResize, {
|
|
1293
|
+
terminalId: msg.terminalId,
|
|
1294
|
+
cols: msg.cols,
|
|
1295
|
+
rows: msg.rows
|
|
1296
|
+
});
|
|
1297
|
+
} else if (msg.kind === "terminal-close") {
|
|
1047
1298
|
deps.gateway.sendEvent(msg.instanceId, MSG2.terminalClose, { terminalId: msg.terminalId });
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
async function handleTerminalOpen(deps, accountId, socket, msg) {
|
|
1302
|
+
const viewerId = deps.webGateway.getViewerId(socket);
|
|
1303
|
+
if (!viewerId) {
|
|
1304
|
+
fail(deps, socket, msg.requestId, msg.instanceId, "terminal-protocol-error", "missing viewer identity");
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
if (!deps.gateway.isOnline(msg.instanceId)) {
|
|
1308
|
+
fail(deps, socket, msg.requestId, msg.instanceId, "instance-offline", "instance is offline");
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
let payload;
|
|
1312
|
+
try {
|
|
1313
|
+
payload = await deps.gateway.sendRequest(msg.instanceId, MSG2.terminalOpen, {
|
|
1314
|
+
chatKey: `relay:${accountId}`,
|
|
1315
|
+
sessionAlias: msg.sessionAlias,
|
|
1316
|
+
viewerId,
|
|
1317
|
+
cols: msg.cols,
|
|
1318
|
+
rows: msg.rows
|
|
1319
|
+
}, { timeoutMs: TERMINAL_REQUEST_TIMEOUT_MS });
|
|
1320
|
+
} catch (err) {
|
|
1321
|
+
const mapped = mapConnectorError(err);
|
|
1322
|
+
fail(deps, socket, msg.requestId, msg.instanceId, mapped.code, mapped.message);
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
if (isErrorPayload(payload)) {
|
|
1326
|
+
fail(deps, socket, msg.requestId, msg.instanceId, payload.error.code, payload.error.message);
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
const result = payload;
|
|
1330
|
+
if (!result || typeof result.terminalId !== "string" || typeof result.generation !== "string" || typeof result.attachmentId !== "string") {
|
|
1331
|
+
fail(deps, socket, msg.requestId, msg.instanceId, "terminal-protocol-error", "malformed open result");
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
if (deps.webGateway.getViewerId(socket) !== viewerId) {
|
|
1335
|
+
detachConnectorAttachment(deps, msg.instanceId, result.attachmentId, viewerId);
|
|
1336
|
+
fail(deps, socket, msg.requestId, msg.instanceId, "terminal-unavailable", "viewer disconnected");
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
try {
|
|
1340
|
+
deps.webGateway.bindAttachment({
|
|
1341
|
+
socket,
|
|
1342
|
+
attachmentId: result.attachmentId,
|
|
1343
|
+
terminalId: result.terminalId,
|
|
1344
|
+
instanceId: msg.instanceId
|
|
1345
|
+
});
|
|
1346
|
+
} catch {
|
|
1347
|
+
detachConnectorAttachment(deps, msg.instanceId, result.attachmentId, viewerId);
|
|
1348
|
+
fail(deps, socket, msg.requestId, msg.instanceId, "terminal-unavailable", "viewer disconnected");
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
const sent = deps.webGateway.send(socket, {
|
|
1352
|
+
kind: "terminal-opened",
|
|
1353
|
+
requestId: msg.requestId,
|
|
1354
|
+
instanceId: msg.instanceId,
|
|
1355
|
+
terminalId: result.terminalId,
|
|
1356
|
+
generation: result.generation,
|
|
1357
|
+
attachmentId: result.attachmentId,
|
|
1358
|
+
role: result.role,
|
|
1359
|
+
viewerCount: result.viewerCount
|
|
1360
|
+
});
|
|
1361
|
+
if (!sent) {
|
|
1362
|
+
deps.webGateway.unbindAttachment(result.attachmentId);
|
|
1363
|
+
detachConnectorAttachment(deps, msg.instanceId, result.attachmentId, viewerId);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
function detachConnectorAttachment(deps, instanceId, attachmentId, viewerId) {
|
|
1367
|
+
deps.gateway.sendEvent(instanceId, MSG2.terminalDetach, {
|
|
1368
|
+
attachmentId,
|
|
1369
|
+
viewerId
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
async function handleTakeControl(deps, socket, msg) {
|
|
1373
|
+
const viewerId = deps.webGateway.getViewerId(socket);
|
|
1374
|
+
if (!viewerId || !deps.webGateway.socketOwnsAttachment(socket, msg.attachmentId)) {
|
|
1375
|
+
fail(deps, socket, msg.requestId, msg.instanceId, "terminal-attachment-not-found", "attachment not bound");
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
try {
|
|
1379
|
+
const payload = await deps.gateway.sendRequest(msg.instanceId, MSG2.terminalTakeControl, { attachmentId: msg.attachmentId, generation: msg.generation, viewerId }, { timeoutMs: TERMINAL_REQUEST_TIMEOUT_MS });
|
|
1380
|
+
if (isErrorPayload(payload)) {
|
|
1381
|
+
fail(deps, socket, msg.requestId, msg.instanceId, payload.error.code, payload.error.message);
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
const result = payload;
|
|
1385
|
+
deps.webGateway.send(socket, {
|
|
1386
|
+
kind: "terminal-opened",
|
|
1387
|
+
requestId: msg.requestId,
|
|
1388
|
+
instanceId: msg.instanceId,
|
|
1389
|
+
terminalId: result.terminalId,
|
|
1390
|
+
generation: result.generation,
|
|
1391
|
+
attachmentId: result.attachmentId,
|
|
1392
|
+
role: result.role,
|
|
1393
|
+
viewerCount: result.viewerCount
|
|
1394
|
+
});
|
|
1395
|
+
} catch (err) {
|
|
1396
|
+
const mapped = mapConnectorError(err);
|
|
1397
|
+
fail(deps, socket, msg.requestId, msg.instanceId, mapped.code, mapped.message);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
async function handleResync(deps, socket, msg) {
|
|
1401
|
+
const viewerId = deps.webGateway.getViewerId(socket);
|
|
1402
|
+
if (!viewerId || !deps.webGateway.socketOwnsAttachment(socket, msg.attachmentId)) {
|
|
1403
|
+
fail(deps, socket, msg.requestId, msg.instanceId, "terminal-attachment-not-found", "attachment not bound");
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
try {
|
|
1407
|
+
const payload = await deps.gateway.sendRequest(msg.instanceId, MSG2.terminalResync, { attachmentId: msg.attachmentId, generation: msg.generation, viewerId }, { timeoutMs: TERMINAL_REQUEST_TIMEOUT_MS });
|
|
1408
|
+
if (isErrorPayload(payload)) {
|
|
1409
|
+
fail(deps, socket, msg.requestId, msg.instanceId, payload.error.code, payload.error.message);
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
deps.webGateway.send(socket, {
|
|
1413
|
+
kind: "terminal-request-failed",
|
|
1414
|
+
requestId: msg.requestId,
|
|
1415
|
+
instanceId: msg.instanceId,
|
|
1416
|
+
code: "ok",
|
|
1417
|
+
message: "resync-accepted"
|
|
1418
|
+
});
|
|
1419
|
+
} catch (err) {
|
|
1420
|
+
const mapped = mapConnectorError(err);
|
|
1421
|
+
fail(deps, socket, msg.requestId, msg.instanceId, mapped.code, mapped.message);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
async function handleTerminate(deps, socket, msg) {
|
|
1425
|
+
try {
|
|
1426
|
+
const payload = await deps.gateway.sendRequest(msg.instanceId, MSG2.terminalTerminate, { terminalId: msg.terminalId, generation: msg.generation }, { timeoutMs: TERMINAL_REQUEST_TIMEOUT_MS });
|
|
1427
|
+
if (isErrorPayload(payload)) {
|
|
1428
|
+
fail(deps, socket, msg.requestId, msg.instanceId, payload.error.code, payload.error.message);
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
const result = payload;
|
|
1432
|
+
deps.webGateway.send(socket, {
|
|
1433
|
+
kind: "terminal-exit",
|
|
1434
|
+
instanceId: msg.instanceId,
|
|
1435
|
+
terminalId: msg.terminalId,
|
|
1436
|
+
generation: msg.generation,
|
|
1437
|
+
reason: result.status === "cleanup-pending" ? "cleanup-pending" : "terminated"
|
|
1438
|
+
});
|
|
1439
|
+
deps.webGateway.send(socket, {
|
|
1440
|
+
kind: "terminal-request-failed",
|
|
1441
|
+
requestId: msg.requestId,
|
|
1442
|
+
instanceId: msg.instanceId,
|
|
1443
|
+
code: result.status,
|
|
1444
|
+
message: result.status
|
|
1445
|
+
});
|
|
1446
|
+
} catch (err) {
|
|
1447
|
+
const mapped = mapConnectorError(err);
|
|
1448
|
+
fail(deps, socket, msg.requestId, msg.instanceId, mapped.code, mapped.message);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
function handleConnectorTerminalEvent(webGateway, instanceId, envelopeType, payload) {
|
|
1452
|
+
if (envelopeType === MSG2.terminalViewerEvent) {
|
|
1453
|
+
const parsed = parseTerminalEventPayload(MSG2.terminalViewerEvent, payload);
|
|
1454
|
+
if (!parsed)
|
|
1455
|
+
return true;
|
|
1456
|
+
const p = parsed;
|
|
1457
|
+
if (!p?.viewerId || !p?.attachmentId || !p?.event)
|
|
1458
|
+
return true;
|
|
1459
|
+
const inner = p.event;
|
|
1460
|
+
let event = null;
|
|
1461
|
+
switch (inner.kind) {
|
|
1462
|
+
case "terminal-rebase-start":
|
|
1463
|
+
event = {
|
|
1464
|
+
kind: "terminal-rebase-start",
|
|
1465
|
+
instanceId,
|
|
1466
|
+
attachmentId: p.attachmentId,
|
|
1467
|
+
generation: inner.generation,
|
|
1468
|
+
epoch: inner.epoch,
|
|
1469
|
+
nextSequence: inner.nextSequence,
|
|
1470
|
+
cols: inner.cols,
|
|
1471
|
+
rows: inner.rows,
|
|
1472
|
+
alternate: inner.alternate,
|
|
1473
|
+
totalBytes: inner.totalBytes,
|
|
1474
|
+
chunkCount: inner.chunkCount
|
|
1475
|
+
};
|
|
1476
|
+
break;
|
|
1477
|
+
case "terminal-rebase-chunk":
|
|
1478
|
+
event = {
|
|
1479
|
+
kind: "terminal-rebase-chunk",
|
|
1480
|
+
instanceId,
|
|
1481
|
+
attachmentId: p.attachmentId,
|
|
1482
|
+
generation: inner.generation,
|
|
1483
|
+
epoch: inner.epoch,
|
|
1484
|
+
index: inner.index,
|
|
1485
|
+
dataBase64: inner.dataBase64
|
|
1486
|
+
};
|
|
1487
|
+
break;
|
|
1488
|
+
case "terminal-rebase-end":
|
|
1489
|
+
event = {
|
|
1490
|
+
kind: "terminal-rebase-end",
|
|
1491
|
+
instanceId,
|
|
1492
|
+
attachmentId: p.attachmentId,
|
|
1493
|
+
generation: inner.generation,
|
|
1494
|
+
epoch: inner.epoch
|
|
1495
|
+
};
|
|
1496
|
+
break;
|
|
1497
|
+
case "terminal-bytes":
|
|
1498
|
+
event = {
|
|
1499
|
+
kind: "terminal-bytes",
|
|
1500
|
+
instanceId,
|
|
1501
|
+
attachmentId: p.attachmentId,
|
|
1502
|
+
generation: inner.generation,
|
|
1503
|
+
epoch: inner.epoch,
|
|
1504
|
+
sequence: inner.sequence,
|
|
1505
|
+
dataBase64: inner.dataBase64
|
|
1506
|
+
};
|
|
1507
|
+
break;
|
|
1508
|
+
case "terminal-role-changed":
|
|
1509
|
+
event = {
|
|
1510
|
+
kind: "terminal-role-changed",
|
|
1511
|
+
instanceId,
|
|
1512
|
+
attachmentId: p.attachmentId,
|
|
1513
|
+
terminalId: inner.terminalId,
|
|
1514
|
+
role: inner.role,
|
|
1515
|
+
viewerCount: inner.viewerCount
|
|
1516
|
+
};
|
|
1517
|
+
break;
|
|
1518
|
+
case "terminal-request-failed":
|
|
1519
|
+
event = {
|
|
1520
|
+
kind: "terminal-request-failed",
|
|
1521
|
+
requestId: inner.requestId ?? "",
|
|
1522
|
+
instanceId,
|
|
1523
|
+
code: inner.code,
|
|
1524
|
+
message: inner.message
|
|
1525
|
+
};
|
|
1526
|
+
break;
|
|
1527
|
+
case "terminal-recovery-failed":
|
|
1528
|
+
event = {
|
|
1529
|
+
kind: "terminal-recovery-failed",
|
|
1530
|
+
instanceId,
|
|
1531
|
+
attachmentId: p.attachmentId,
|
|
1532
|
+
generation: inner.generation,
|
|
1533
|
+
code: inner.code,
|
|
1534
|
+
message: inner.message
|
|
1535
|
+
};
|
|
1536
|
+
break;
|
|
1537
|
+
default:
|
|
1538
|
+
return true;
|
|
1539
|
+
}
|
|
1540
|
+
if (event)
|
|
1541
|
+
webGateway.sendToAttachment(p.viewerId, p.attachmentId, event);
|
|
1542
|
+
return true;
|
|
1543
|
+
}
|
|
1544
|
+
if (envelopeType === MSG2.terminalResourceExit) {
|
|
1545
|
+
const parsed = parseTerminalEventPayload(MSG2.terminalResourceExit, payload);
|
|
1546
|
+
if (!parsed)
|
|
1547
|
+
return true;
|
|
1548
|
+
const p = parsed;
|
|
1549
|
+
if (!p?.terminalId || !p?.generation)
|
|
1550
|
+
return true;
|
|
1551
|
+
webGateway.fanoutTerminalExit(instanceId, p.terminalId, {
|
|
1552
|
+
kind: "terminal-exit",
|
|
1553
|
+
instanceId,
|
|
1554
|
+
terminalId: p.terminalId,
|
|
1555
|
+
generation: p.generation,
|
|
1556
|
+
reason: p.reason,
|
|
1557
|
+
...p.code !== undefined ? { code: p.code } : {}
|
|
1558
|
+
});
|
|
1559
|
+
return true;
|
|
1560
|
+
}
|
|
1561
|
+
return false;
|
|
1048
1562
|
}
|
|
1049
1563
|
|
|
1050
1564
|
// packages/relay/src/http/app.ts
|
|
1051
1565
|
import { Hono } from "hono";
|
|
1052
1566
|
import { deleteCookie, getCookie, setCookie } from "hono/cookie";
|
|
1053
1567
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
1054
|
-
import { randomUUID as
|
|
1055
|
-
import { isErrorPayload, MSG as MSG3, parseControlPayload } from "@ganglion/xacpx-relay-protocol";
|
|
1568
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
1569
|
+
import { isErrorPayload as isErrorPayload2, MSG as MSG3, parseControlPayload } from "@ganglion/xacpx-relay-protocol";
|
|
1056
1570
|
|
|
1057
1571
|
// packages/relay/src/http/client-ip.ts
|
|
1058
1572
|
import { getConnInfo } from "@hono/node-server/conninfo";
|
|
@@ -1583,7 +2097,7 @@ function createApp(deps) {
|
|
|
1583
2097
|
...previewUrl ? { previewUrl } : {}
|
|
1584
2098
|
};
|
|
1585
2099
|
});
|
|
1586
|
-
const promptRequestId =
|
|
2100
|
+
const promptRequestId = randomUUID4();
|
|
1587
2101
|
persistedPromptId = deps.messages.append(instance.id, p.sessionAlias, "in", p.text, undefined, attachments, promptRequestId);
|
|
1588
2102
|
if (body.type === MSG3.prompt && typeof payload === "object" && payload !== null) {
|
|
1589
2103
|
payload.promptRequestId = promptRequestId;
|
|
@@ -1599,7 +2113,7 @@ function createApp(deps) {
|
|
|
1599
2113
|
queueItemId: result.queueItemId
|
|
1600
2114
|
});
|
|
1601
2115
|
}
|
|
1602
|
-
if (removeInput && !
|
|
2116
|
+
if (removeInput && !isErrorPayload2(result)) {
|
|
1603
2117
|
deps.messages.deleteBySession(instance.id, removeInput.alias);
|
|
1604
2118
|
}
|
|
1605
2119
|
if (body.type === MSG3.commandExecute) {
|
|
@@ -1733,7 +2247,16 @@ async function createRelayRuntime(dbPath, options = {}) {
|
|
|
1733
2247
|
const accounts = new AccountStore(db);
|
|
1734
2248
|
const instances = new InstanceStore(db);
|
|
1735
2249
|
const messages = new MessageStore(db);
|
|
1736
|
-
|
|
2250
|
+
let gatewayRef = null;
|
|
2251
|
+
const webGateway = new WebGateway({
|
|
2252
|
+
logger,
|
|
2253
|
+
onAttachmentDetached: (info) => {
|
|
2254
|
+
gatewayRef?.sendEvent(info.instanceId, MSG4.terminalDetach, {
|
|
2255
|
+
attachmentId: info.attachmentId,
|
|
2256
|
+
viewerId: info.viewerId
|
|
2257
|
+
});
|
|
2258
|
+
}
|
|
2259
|
+
});
|
|
1737
2260
|
const turnBuffers = new Map;
|
|
1738
2261
|
const key = (instanceId, alias) => `${instanceId}\x00${alias}`;
|
|
1739
2262
|
const recoveredFingerprints = new Set;
|
|
@@ -1832,6 +2355,9 @@ async function createRelayRuntime(dbPath, options = {}) {
|
|
|
1832
2355
|
webGateway.broadcast(accountId, { kind: "instance-status", instanceId, online });
|
|
1833
2356
|
},
|
|
1834
2357
|
onEvent: (instanceId, accountId, envelope) => {
|
|
2358
|
+
if (handleConnectorTerminalEvent(webGateway, instanceId, envelope.type, envelope.payload)) {
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
1835
2361
|
const recentRows = (sessionAlias) => messages.listBySession(accountId, instanceId, sessionAlias, { limit: 5 }).messages;
|
|
1836
2362
|
const hasTrailingPrompt = (sessionAlias, prompt) => {
|
|
1837
2363
|
const rows = recentRows(sessionAlias);
|
|
@@ -2086,6 +2612,7 @@ async function createRelayRuntime(dbPath, options = {}) {
|
|
|
2086
2612
|
}
|
|
2087
2613
|
}
|
|
2088
2614
|
});
|
|
2615
|
+
gatewayRef = gateway;
|
|
2089
2616
|
const app = createApp({
|
|
2090
2617
|
accounts,
|
|
2091
2618
|
instances,
|
|
@@ -2329,7 +2856,7 @@ async function runRelayCli(args, io) {
|
|
|
2329
2856
|
const label = flag(args, "--label");
|
|
2330
2857
|
const runtime = await createRelayRuntime(dbPath);
|
|
2331
2858
|
try {
|
|
2332
|
-
const username = "u-" +
|
|
2859
|
+
const username = "u-" + randomUUID5();
|
|
2333
2860
|
const acc = runtime.accounts.createAccount(username);
|
|
2334
2861
|
const { token } = runtime.accounts.createLoginToken(acc.id, label);
|
|
2335
2862
|
io.print(`access token: ${token}`);
|