@ganglion/xacpx-relay 0.14.7 → 0.14.9-beta.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 +1157 -85
- package/dist/gateway/desktop-stream-gateway.d.ts +150 -0
- package/dist/gateway/desktop-stream-registry.d.ts +79 -0
- package/dist/gateway/desktop-ticket-store.d.ts +37 -0
- package/dist/gateway/desktop-viewer-cancel.d.ts +73 -0
- package/dist/gateway/instance-gateway.d.ts +30 -1
- package/dist/gateway/web-gateway.d.ts +7 -0
- package/dist/gateway/web-inbound.d.ts +50 -2
- package/dist/http/app.d.ts +32 -3
- package/dist/interaction-registry.d.ts +171 -0
- package/dist/relay-web/assets/DashboardView-BOjJZyPa.js +536 -0
- package/dist/relay-web/assets/DashboardView-CxbB9aOt.css +1 -0
- package/dist/relay-web/assets/InviteView-BrpEZ62C.js +1 -0
- package/dist/relay-web/assets/InviteView-J2ktfVru.css +1 -0
- package/dist/relay-web/assets/LoginView-ClWbwt-c.js +1 -0
- package/dist/relay-web/assets/LoginView-kC8xjA_t.css +1 -0
- package/dist/relay-web/assets/SettingsView-CgRvKsTu.js +6 -0
- package/dist/relay-web/assets/{abnfDiagram-VRR7QNED-DaKwODLI.js → abnfDiagram-VRR7QNED-Dck3JZYa.js} +1 -1
- package/dist/relay-web/assets/{arc-CV9VS-uW.js → arc-BUwGn8Aw.js} +1 -1
- package/dist/relay-web/assets/architectureDiagram-ZJ3FMSHR-xpEg81LX.js +36 -0
- package/dist/relay-web/assets/blockDiagram-677ZJIJ3-BubzF_yu.js +132 -0
- package/dist/relay-web/assets/c4Diagram-LMCZKHZV-DuWTAHtf.js +10 -0
- package/dist/relay-web/assets/channel-BgdSN6lg.js +1 -0
- package/dist/relay-web/assets/chunk-2Q5K7J3B-DBDlij9L.js +1 -0
- package/dist/relay-web/assets/{chunk-32BRIVSS-DPN74KhB.js → chunk-32BRIVSS-BVy-gEXe.js} +1 -1
- package/dist/relay-web/assets/{chunk-5VM5RSS4-DfDDB13s.js → chunk-5VM5RSS4-lG5wevp_.js} +1 -1
- package/dist/relay-web/assets/chunk-EX3LRPZG-C39iM1_G.js +231 -0
- package/dist/relay-web/assets/chunk-JWPE2WC7-CXMvPmeE.js +1 -0
- package/dist/relay-web/assets/chunk-MOJQB5TN-1nAmtQu2.js +88 -0
- package/dist/relay-web/assets/chunk-RYQCIY6F-bEx1WMva.js +1 -0
- package/dist/relay-web/assets/chunk-V7JOEXUC-Ca-ze-xM.js +206 -0
- package/dist/relay-web/assets/chunk-VR4S4FIN-BLJsBZDp.js +1 -0
- package/dist/relay-web/assets/{chunk-XXDRQBXY-DJGRRjV3.js → chunk-XXDRQBXY-bsEWv2ey.js} +1 -1
- package/dist/relay-web/assets/classDiagram-OUVF2IWQ-DGFgm9zH.js +1 -0
- package/dist/relay-web/assets/classDiagram-v2-EOCWNBFH-DGFgm9zH.js +1 -0
- package/dist/relay-web/assets/cose-bilkent-JH36ORCC-B6jSr2Nv.js +1 -0
- package/dist/relay-web/assets/cynefin-VYW2F7L2-oysjrmsP.js +178 -0
- package/dist/relay-web/assets/{cynefinDiagram-TSTJHNR4-BNpeh400.js → cynefinDiagram-TSTJHNR4-s5Y1Nshk.js} +1 -1
- package/dist/relay-web/assets/{cytoscape.esm-CUqq0XTU.js → cytoscape.esm-DJK6lGiH.js} +3 -3
- package/dist/relay-web/assets/dagre-VKFMJZFB-xovP-3AF.js +4 -0
- package/dist/relay-web/assets/diagram-FQU43EPY-BSpqL1K9.js +3 -0
- package/dist/relay-web/assets/diagram-G47NLZAW-DqnFRysq.js +24 -0
- package/dist/relay-web/assets/diagram-NH7WQ7WH-BBuDaT9C.js +24 -0
- package/dist/relay-web/assets/diagram-OA4YK3LP-BsK3Ep2f.js +30 -0
- package/dist/relay-web/assets/diagram-WEI45ONY-BjW00xkP.js +41 -0
- package/dist/relay-web/assets/{ebnfDiagram-CCIWWBDH-DDO0kQPX.js → ebnfDiagram-CCIWWBDH-D9pd5un2.js} +1 -1
- package/dist/relay-web/assets/erDiagram-Q63AITRT-BpiIjChz.js +85 -0
- package/dist/relay-web/assets/flowDiagram-23GEKE2U-B_peH4w6.js +156 -0
- package/dist/relay-web/assets/{ganttDiagram-NO4QXBWP-bSsTF0DT.js → ganttDiagram-NO4QXBWP-BwQ4FW2W.js} +1 -1
- package/dist/relay-web/assets/gitGraphDiagram-IHSO6WYX-DlGg5kSb.js +106 -0
- package/dist/relay-web/assets/graph-gxHQN9-b.js +1 -0
- package/dist/relay-web/assets/index-CHcIz6UQ.js +151 -0
- package/dist/relay-web/assets/index-DEaRIAEf.css +1 -0
- package/dist/relay-web/assets/{infoDiagram-FWYZ7A6U-CmX6hKNb.js → infoDiagram-FWYZ7A6U-Bthqui0m.js} +1 -1
- package/dist/relay-web/assets/ishikawaDiagram-FXEZZL3T-BBb-68i0.js +70 -0
- package/dist/relay-web/assets/{journeyDiagram-5HDEW3XC-b7YoZRs-.js → journeyDiagram-5HDEW3XC-C9vRiAK6.js} +1 -1
- package/dist/relay-web/assets/kanban-definition-HUTT4EX6-Bgbviu-S.js +89 -0
- package/dist/relay-web/assets/{katex-C5jXJg4s.js → katex-HP8lGamR.js} +1 -1
- package/dist/relay-web/assets/{layout-HEHzrzvT.js → layout-CpcJW2by.js} +1 -1
- package/dist/relay-web/assets/{linear-qgwU0RG6.js → linear-Bswmlg_d.js} +1 -1
- package/dist/relay-web/assets/{map-BQHxRNoK.js → map-CKPHyBGT.js} +1 -1
- package/dist/relay-web/assets/mermaid.core-BCis0YlZ.js +312 -0
- package/dist/relay-web/assets/mindmap-definition-LN4V7U3C-DZWl9sWH.js +96 -0
- package/dist/relay-web/assets/{pegDiagram-2B236MQR-Cfx_WRNK.js → pegDiagram-2B236MQR-fbbfPmLp.js} +1 -1
- package/dist/relay-web/assets/pieDiagram-ENE6RG2P-8ANoKukC.js +39 -0
- package/dist/relay-web/assets/quadrantDiagram-ABIIQ3AL-Da3tBy4o.js +7 -0
- package/dist/relay-web/assets/{railroadDiagram-RFXS5EU6-BwasTCtU.js → railroadDiagram-RFXS5EU6-B9qdvKhl.js} +1 -1
- package/dist/relay-web/assets/requirementDiagram-TGXJPOKE-CWN6fXGC.js +84 -0
- package/dist/relay-web/assets/rfb-6tS6afZT.js +5 -0
- package/dist/relay-web/assets/sankeyDiagram-HTMAVEWB-GAdVFhEf.js +40 -0
- package/dist/relay-web/assets/sequenceDiagram-DBY2YBRQ-BsjsMh_N.js +162 -0
- package/dist/relay-web/assets/sizeCapture-X5ZJPWSS-BYqM-l-h.js +1 -0
- package/dist/relay-web/assets/{stateDiagram-2N3HPSRC-6J7enxP8.js → stateDiagram-2N3HPSRC-CYC4lpgV.js} +1 -1
- package/dist/relay-web/assets/stateDiagram-v2-6OUMAXLB-BFB2bl9b.js +1 -0
- package/dist/relay-web/assets/swimlanes-5IMT3BWC-DMkHrrNX.js +2 -0
- package/dist/relay-web/assets/swimlanesDiagram-G3AALYLV-BiLcwTEU.js +8 -0
- package/dist/relay-web/assets/theme-CRHsNTDg.js +1 -0
- package/dist/relay-web/assets/timeline-definition-FHXFAJF6-DWlf0bUt.js +120 -0
- package/dist/relay-web/assets/vennDiagram-L72KCM5P-CbdnfByf.js +34 -0
- package/dist/relay-web/assets/wardleyDiagram-EHGQE667-BCvSYtNJ.js +78 -0
- package/dist/relay-web/assets/xterm-CASmyfyk.js +27 -0
- package/dist/relay-web/assets/xterm-MhP_vFvK.css +32 -0
- package/dist/relay-web/assets/xychartDiagram-FW5EYKEG-BnKnBAs8.js +7 -0
- package/dist/relay-web/index.html +2 -2
- package/dist/relay-web/sw.js +1 -1
- package/dist/server.d.ts +25 -0
- package/package.json +2 -2
- package/dist/relay-web/assets/DashboardView-4IA9qgyK.js +0 -474
- package/dist/relay-web/assets/DashboardView-BhtuSo0h.css +0 -1
- package/dist/relay-web/assets/InviteView-Dz5OA4kl.js +0 -1
- package/dist/relay-web/assets/InviteView-hJ3Eqm6F.css +0 -1
- package/dist/relay-web/assets/LoginView-BDsZDCu4.css +0 -1
- package/dist/relay-web/assets/LoginView-DSkzpE2J.js +0 -1
- package/dist/relay-web/assets/SettingsView-Dep66dxc.js +0 -6
- package/dist/relay-web/assets/architectureDiagram-ZJ3FMSHR-CbaahLkm.js +0 -36
- package/dist/relay-web/assets/blockDiagram-677ZJIJ3-56mgVcyf.js +0 -132
- package/dist/relay-web/assets/c4Diagram-LMCZKHZV-dmZoIhGg.js +0 -10
- package/dist/relay-web/assets/channel-DZd0OnOW.js +0 -1
- package/dist/relay-web/assets/chunk-2Q5K7J3B-DlTJ10nZ.js +0 -1
- package/dist/relay-web/assets/chunk-EX3LRPZG-C3nLlpx-.js +0 -231
- package/dist/relay-web/assets/chunk-JWPE2WC7-BWYtSFDI.js +0 -1
- package/dist/relay-web/assets/chunk-MOJQB5TN-B7zlbyBm.js +0 -88
- package/dist/relay-web/assets/chunk-RYQCIY6F-CntGwtzD.js +0 -1
- package/dist/relay-web/assets/chunk-V7JOEXUC-Bp_pqLJZ.js +0 -206
- package/dist/relay-web/assets/chunk-VR4S4FIN-BKVoyZce.js +0 -1
- package/dist/relay-web/assets/classDiagram-OUVF2IWQ-B5R0Hfzs.js +0 -1
- package/dist/relay-web/assets/classDiagram-v2-EOCWNBFH-B5R0Hfzs.js +0 -1
- package/dist/relay-web/assets/cose-bilkent-JH36ORCC-CVFX_TmM.js +0 -1
- package/dist/relay-web/assets/cynefin-VYW2F7L2-DSm8ksAf.js +0 -178
- package/dist/relay-web/assets/dagre-VKFMJZFB-DE1k216e.js +0 -4
- package/dist/relay-web/assets/diagram-FQU43EPY-h2bdCdUk.js +0 -3
- package/dist/relay-web/assets/diagram-G47NLZAW-BXdW-FiW.js +0 -24
- package/dist/relay-web/assets/diagram-NH7WQ7WH-CRJggoib.js +0 -24
- package/dist/relay-web/assets/diagram-OA4YK3LP-BTJubaWI.js +0 -30
- package/dist/relay-web/assets/diagram-WEI45ONY-COYBEkjL.js +0 -41
- package/dist/relay-web/assets/erDiagram-Q63AITRT-E2feMDQa.js +0 -85
- package/dist/relay-web/assets/flowDiagram-23GEKE2U-sHrond1v.js +0 -156
- package/dist/relay-web/assets/gitGraphDiagram-IHSO6WYX-Dfftd6A5.js +0 -106
- package/dist/relay-web/assets/graph-C2PyVmSI.js +0 -1
- package/dist/relay-web/assets/index-B0GiuYnP.css +0 -1
- package/dist/relay-web/assets/index-CCsE2eVx.js +0 -151
- package/dist/relay-web/assets/ishikawaDiagram-FXEZZL3T-CEafVlSP.js +0 -70
- package/dist/relay-web/assets/kanban-definition-HUTT4EX6-CQJjV0wH.js +0 -89
- package/dist/relay-web/assets/mermaid.core-BWC59kZs.js +0 -312
- package/dist/relay-web/assets/mindmap-definition-LN4V7U3C-BIqI_g8X.js +0 -96
- package/dist/relay-web/assets/pieDiagram-ENE6RG2P-BE3DnzCC.js +0 -39
- package/dist/relay-web/assets/quadrantDiagram-ABIIQ3AL-BRnaz8YE.js +0 -7
- package/dist/relay-web/assets/requirementDiagram-TGXJPOKE-vNkYyFU7.js +0 -84
- package/dist/relay-web/assets/sankeyDiagram-HTMAVEWB-C_EfSDJD.js +0 -40
- package/dist/relay-web/assets/sequenceDiagram-DBY2YBRQ-s4llcEpj.js +0 -162
- package/dist/relay-web/assets/sizeCapture-X5ZJPWSS-DvaH8gFI.js +0 -1
- package/dist/relay-web/assets/stateDiagram-v2-6OUMAXLB-laWLPKHD.js +0 -1
- package/dist/relay-web/assets/swimlanes-5IMT3BWC-BQCukg8Z.js +0 -2
- package/dist/relay-web/assets/swimlanesDiagram-G3AALYLV-OigEZaOT.js +0 -8
- package/dist/relay-web/assets/theme-_eLYZJbT.js +0 -1
- package/dist/relay-web/assets/timeline-definition-FHXFAJF6-BtoktOBS.js +0 -120
- package/dist/relay-web/assets/vennDiagram-L72KCM5P-N_hCOD0c.js +0 -34
- package/dist/relay-web/assets/wardleyDiagram-EHGQE667-AmQ55zTN.js +0 -78
- package/dist/relay-web/assets/xterm-9CEnUXvW.css +0 -32
- package/dist/relay-web/assets/xterm-BT7tBf8A.js +0 -27
- package/dist/relay-web/assets/xychartDiagram-FW5EYKEG-CXkp2O4G.js +0 -7
package/dist/cli.js
CHANGED
|
@@ -7251,11 +7251,15 @@ import { dirname as dirname3, join as join2, resolve } from "node:path";
|
|
|
7251
7251
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
7252
7252
|
|
|
7253
7253
|
// packages/relay/src/server.ts
|
|
7254
|
+
import { createServer } from "node:http";
|
|
7254
7255
|
import { serve } from "@hono/node-server";
|
|
7255
7256
|
import { WebSocketServer } from "ws";
|
|
7256
7257
|
import {
|
|
7258
|
+
DESKTOP_TICKET_TTL_MS as DESKTOP_TICKET_TTL_MS2,
|
|
7259
|
+
DESKTOP_WS_MAX_PAYLOAD_BYTES as DESKTOP_WS_MAX_PAYLOAD_BYTES2,
|
|
7260
|
+
MAX_DESKTOP_TICKET_LENGTH as MAX_DESKTOP_TICKET_LENGTH2,
|
|
7257
7261
|
MAX_TOOL_STEPS,
|
|
7258
|
-
MSG as
|
|
7262
|
+
MSG as MSG5,
|
|
7259
7263
|
REASONING_CAP,
|
|
7260
7264
|
STATE_SYNC_PARTS_CAP,
|
|
7261
7265
|
STATE_SYNC_TEXT_CAP,
|
|
@@ -8175,9 +8179,141 @@ import {
|
|
|
8175
8179
|
decodeEnvelope,
|
|
8176
8180
|
encodeEnvelope,
|
|
8177
8181
|
errorPayload,
|
|
8178
|
-
normalizeCapabilities
|
|
8182
|
+
normalizeCapabilities,
|
|
8183
|
+
validateInteractionRequest,
|
|
8184
|
+
validateInteractionWithdraw
|
|
8179
8185
|
} from "@ganglion/xacpx-relay-protocol";
|
|
8180
8186
|
|
|
8187
|
+
// packages/relay/src/interaction-registry.ts
|
|
8188
|
+
function interactionResultForBrowser(result, accountId) {
|
|
8189
|
+
if (typeof result !== "object" || result === null) {
|
|
8190
|
+
return { responded: false, reason: "aborted" };
|
|
8191
|
+
}
|
|
8192
|
+
const outcome = result;
|
|
8193
|
+
if (outcome.responded !== true) {
|
|
8194
|
+
const reason = typeof outcome.reason === "string" ? outcome.reason : "aborted";
|
|
8195
|
+
return {
|
|
8196
|
+
responded: false,
|
|
8197
|
+
reason: reason === "timeout" || reason === "aborted" || reason === "shutdown" || reason === "unsupported" || reason === "channel-missing" ? reason : "aborted"
|
|
8198
|
+
};
|
|
8199
|
+
}
|
|
8200
|
+
const response = outcome.response;
|
|
8201
|
+
if (typeof response !== "object" || response === null) {
|
|
8202
|
+
return { responded: false, reason: "aborted" };
|
|
8203
|
+
}
|
|
8204
|
+
const decision = response;
|
|
8205
|
+
return {
|
|
8206
|
+
responded: true,
|
|
8207
|
+
response: { ...decision, responderId: accountId }
|
|
8208
|
+
};
|
|
8209
|
+
}
|
|
8210
|
+
|
|
8211
|
+
class InteractionRegistry {
|
|
8212
|
+
logger;
|
|
8213
|
+
pending = new Map;
|
|
8214
|
+
closedListeners = new Set;
|
|
8215
|
+
constructor(logger) {
|
|
8216
|
+
this.logger = logger;
|
|
8217
|
+
}
|
|
8218
|
+
onClose(listener) {
|
|
8219
|
+
this.closedListeners.add(listener);
|
|
8220
|
+
return () => {
|
|
8221
|
+
this.closedListeners.delete(listener);
|
|
8222
|
+
};
|
|
8223
|
+
}
|
|
8224
|
+
get size() {
|
|
8225
|
+
return this.pending.size;
|
|
8226
|
+
}
|
|
8227
|
+
open(entry) {
|
|
8228
|
+
const withTimer = { ...entry, closed: false };
|
|
8229
|
+
withTimer.timer = setTimeout(() => {
|
|
8230
|
+
this.close(entry.requestId, "expired");
|
|
8231
|
+
}, Math.max(0, entry.answerWindowMs));
|
|
8232
|
+
if (typeof withTimer.timer.unref === "function")
|
|
8233
|
+
withTimer.timer.unref();
|
|
8234
|
+
this.pending.set(entry.requestId, withTimer);
|
|
8235
|
+
this.logger.debug("relay.interaction.opened", "interaction opened", {
|
|
8236
|
+
requestId: entry.requestId,
|
|
8237
|
+
instanceId: entry.instanceId,
|
|
8238
|
+
kind: entry.kind
|
|
8239
|
+
});
|
|
8240
|
+
}
|
|
8241
|
+
answer(requestId, decision) {
|
|
8242
|
+
const entry = this.pending.get(requestId);
|
|
8243
|
+
if (!entry || entry.closed)
|
|
8244
|
+
return null;
|
|
8245
|
+
if (entry.kind !== decision.kind) {
|
|
8246
|
+
return null;
|
|
8247
|
+
}
|
|
8248
|
+
if (Date.now() >= entry.expiresAt) {
|
|
8249
|
+
this.close(requestId, "expired");
|
|
8250
|
+
return null;
|
|
8251
|
+
}
|
|
8252
|
+
this.finish(entry, "resolved", decision.action);
|
|
8253
|
+
entry.resolve(decision);
|
|
8254
|
+
return entry;
|
|
8255
|
+
}
|
|
8256
|
+
close(requestId, reason) {
|
|
8257
|
+
const entry = this.pending.get(requestId);
|
|
8258
|
+
if (!entry || entry.closed)
|
|
8259
|
+
return false;
|
|
8260
|
+
this.finish(entry, reason);
|
|
8261
|
+
entry.reject(reason);
|
|
8262
|
+
return true;
|
|
8263
|
+
}
|
|
8264
|
+
listForInstance(instanceId) {
|
|
8265
|
+
const entries = [];
|
|
8266
|
+
for (const entry of this.pending.values()) {
|
|
8267
|
+
if (entry.instanceId === instanceId)
|
|
8268
|
+
entries.push(entry);
|
|
8269
|
+
}
|
|
8270
|
+
return entries;
|
|
8271
|
+
}
|
|
8272
|
+
listForAccount(accountId) {
|
|
8273
|
+
const entries = [];
|
|
8274
|
+
for (const entry of this.pending.values()) {
|
|
8275
|
+
if (entry.accountId === accountId)
|
|
8276
|
+
entries.push(entry);
|
|
8277
|
+
}
|
|
8278
|
+
return entries;
|
|
8279
|
+
}
|
|
8280
|
+
get(requestId) {
|
|
8281
|
+
const entry = this.pending.get(requestId);
|
|
8282
|
+
return entry && !entry.closed ? entry : null;
|
|
8283
|
+
}
|
|
8284
|
+
finish(entry, reason, action) {
|
|
8285
|
+
entry.closed = true;
|
|
8286
|
+
if (entry.timer !== undefined) {
|
|
8287
|
+
clearTimeout(entry.timer);
|
|
8288
|
+
entry.timer = undefined;
|
|
8289
|
+
}
|
|
8290
|
+
this.pending.delete(entry.requestId);
|
|
8291
|
+
this.logger.debug("relay.interaction.closed", "interaction closed", {
|
|
8292
|
+
requestId: entry.requestId,
|
|
8293
|
+
instanceId: entry.instanceId,
|
|
8294
|
+
reason,
|
|
8295
|
+
...action !== undefined ? { action } : {}
|
|
8296
|
+
});
|
|
8297
|
+
for (const listener of this.closedListeners) {
|
|
8298
|
+
try {
|
|
8299
|
+
listener({
|
|
8300
|
+
requestId: entry.requestId,
|
|
8301
|
+
instanceId: entry.instanceId,
|
|
8302
|
+
accountId: entry.accountId,
|
|
8303
|
+
kind: entry.kind,
|
|
8304
|
+
reason,
|
|
8305
|
+
...action !== undefined ? { action } : {}
|
|
8306
|
+
});
|
|
8307
|
+
} catch (error) {
|
|
8308
|
+
this.logger.debug("relay.interaction.close_listener_failed", "interaction close listener threw", {
|
|
8309
|
+
requestId: entry.requestId,
|
|
8310
|
+
message: error instanceof Error ? error.message : String(error)
|
|
8311
|
+
});
|
|
8312
|
+
}
|
|
8313
|
+
}
|
|
8314
|
+
}
|
|
8315
|
+
}
|
|
8316
|
+
|
|
8181
8317
|
// packages/relay/src/logging.ts
|
|
8182
8318
|
var ORDER = { error: 0, warn: 1, info: 2, debug: 3 };
|
|
8183
8319
|
function createNoopRelayLogger() {
|
|
@@ -8339,6 +8475,9 @@ class InstanceGateway {
|
|
|
8339
8475
|
isOnline(instanceId) {
|
|
8340
8476
|
return this.connections.has(instanceId);
|
|
8341
8477
|
}
|
|
8478
|
+
broadcastControlEvent(accountId, event) {
|
|
8479
|
+
this.deps.broadcastControlEvent?.(accountId, event);
|
|
8480
|
+
}
|
|
8342
8481
|
handleConnection(socket) {
|
|
8343
8482
|
let authed = null;
|
|
8344
8483
|
startHeartbeat(socket, this.deps.heartbeatIntervalMs, undefined, this.logger);
|
|
@@ -8374,6 +8513,12 @@ class InstanceGateway {
|
|
|
8374
8513
|
p.reject(new Error("instance-offline"));
|
|
8375
8514
|
}
|
|
8376
8515
|
}
|
|
8516
|
+
const interactions = this.deps.interactions;
|
|
8517
|
+
if (interactions) {
|
|
8518
|
+
for (const entry of interactions.listForInstance(instanceId)) {
|
|
8519
|
+
interactions.close(entry.requestId, "withdrawn");
|
|
8520
|
+
}
|
|
8521
|
+
}
|
|
8377
8522
|
this.deps.onStatusChange?.(instanceId, accountId, false);
|
|
8378
8523
|
this.broadcastDirectorySnapshot(accountId);
|
|
8379
8524
|
}
|
|
@@ -8622,6 +8767,76 @@ class InstanceGateway {
|
|
|
8622
8767
|
}).catch((err) => respond(errorPayload(err.code ?? "DELIVERY_FAILED", err instanceof Error ? err.message : String(err))));
|
|
8623
8768
|
return;
|
|
8624
8769
|
}
|
|
8770
|
+
if (envelope.type === MSG.interactionRequest) {
|
|
8771
|
+
const parsed = validateInteractionRequest(envelope.payload);
|
|
8772
|
+
if (!parsed) {
|
|
8773
|
+
respond(errorPayload("invalid-payload", `${MSG.interactionRequest}: malformed payload`));
|
|
8774
|
+
return;
|
|
8775
|
+
}
|
|
8776
|
+
const registry = this.deps.interactions;
|
|
8777
|
+
if (!registry) {
|
|
8778
|
+
respond(errorPayload("interaction-unavailable", "interaction registry is not configured"));
|
|
8779
|
+
return;
|
|
8780
|
+
}
|
|
8781
|
+
const instanceId = authed.instanceId;
|
|
8782
|
+
const accountId = authed.accountId;
|
|
8783
|
+
const chatKey = `relay:${accountId}`;
|
|
8784
|
+
const requestId = parsed.requestId;
|
|
8785
|
+
const now = Date.now();
|
|
8786
|
+
if (parsed.expiresAt <= now) {
|
|
8787
|
+
respond({ responded: false, reason: "timeout" });
|
|
8788
|
+
return;
|
|
8789
|
+
}
|
|
8790
|
+
if (parsed.kind !== "elicitation") {
|
|
8791
|
+
respond({ responded: false, reason: "unsupported" });
|
|
8792
|
+
return;
|
|
8793
|
+
}
|
|
8794
|
+
const answerWindowMs = parsed.expiresAt - now;
|
|
8795
|
+
registry.open({
|
|
8796
|
+
requestId,
|
|
8797
|
+
instanceId,
|
|
8798
|
+
accountId,
|
|
8799
|
+
kind: parsed.kind,
|
|
8800
|
+
expiresAt: parsed.expiresAt,
|
|
8801
|
+
chatKey,
|
|
8802
|
+
sessionAlias: "",
|
|
8803
|
+
answerWindowMs,
|
|
8804
|
+
timeoutMs: answerWindowMs + REQUEST_RESPONSE_RESERVE_MS,
|
|
8805
|
+
...parsed.conversation !== undefined ? { conversation: parsed.conversation } : {},
|
|
8806
|
+
...parsed.elicitation !== undefined ? { elicitation: parsed.elicitation } : {},
|
|
8807
|
+
...parsed.permission !== undefined ? { permission: parsed.permission } : {},
|
|
8808
|
+
resolve: (decision) => respond(interactionResultForBrowser({ responded: true, response: decision }, accountId)),
|
|
8809
|
+
reject: (reason) => respond({ responded: false, reason: reason === "expired" ? "timeout" : "withdrawn" })
|
|
8810
|
+
});
|
|
8811
|
+
this.deps.broadcastControlEvent?.(accountId, {
|
|
8812
|
+
type: "interaction-opened",
|
|
8813
|
+
chatKey,
|
|
8814
|
+
sessionAlias: "",
|
|
8815
|
+
instanceId,
|
|
8816
|
+
interaction: parsed
|
|
8817
|
+
});
|
|
8818
|
+
return;
|
|
8819
|
+
}
|
|
8820
|
+
if (envelope.type === MSG.interactionWithdraw) {
|
|
8821
|
+
const parsed = validateInteractionWithdraw(envelope.payload);
|
|
8822
|
+
if (!parsed) {
|
|
8823
|
+
respond(errorPayload("invalid-payload", `${MSG.interactionWithdraw}: malformed payload`));
|
|
8824
|
+
return;
|
|
8825
|
+
}
|
|
8826
|
+
const registry = this.deps.interactions;
|
|
8827
|
+
if (!registry) {
|
|
8828
|
+
respond(errorPayload("interaction-unavailable", "interaction registry is not configured"));
|
|
8829
|
+
return;
|
|
8830
|
+
}
|
|
8831
|
+
const pending = registry.get(parsed.requestId);
|
|
8832
|
+
if (!pending || pending.instanceId !== authed.instanceId || pending.accountId !== authed.accountId) {
|
|
8833
|
+
respond({ ok: true });
|
|
8834
|
+
return;
|
|
8835
|
+
}
|
|
8836
|
+
registry.close(parsed.requestId, "withdrawn");
|
|
8837
|
+
respond({ ok: true });
|
|
8838
|
+
return;
|
|
8839
|
+
}
|
|
8625
8840
|
respond(errorPayload("invalid-request", `unknown request type: ${envelope.type}`));
|
|
8626
8841
|
return;
|
|
8627
8842
|
}
|
|
@@ -8816,6 +9031,7 @@ class WebGateway {
|
|
|
8816
9031
|
this.options.logger?.debug("relay.web.connected", "web client connected", { accountId });
|
|
8817
9032
|
startHeartbeat(socket, this.options.heartbeatIntervalMs, undefined, this.options.logger);
|
|
8818
9033
|
socket.on("close", () => {
|
|
9034
|
+
const closedViewerId = this.viewerBySocket.get(socket);
|
|
8819
9035
|
this.clearSocketAttachments(socket);
|
|
8820
9036
|
set.delete(socket);
|
|
8821
9037
|
this.subscriptions.delete(socket);
|
|
@@ -8824,6 +9040,8 @@ class WebGateway {
|
|
|
8824
9040
|
this.attachmentsBySocket.delete(socket);
|
|
8825
9041
|
if (set.size === 0)
|
|
8826
9042
|
this.byAccount.delete(accountId);
|
|
9043
|
+
if (closedViewerId)
|
|
9044
|
+
this.options.onViewerClosed?.(closedViewerId);
|
|
8827
9045
|
this.options.logger?.debug("relay.web.disconnected", "web client disconnected", { accountId });
|
|
8828
9046
|
});
|
|
8829
9047
|
return viewerId;
|
|
@@ -8956,6 +9174,443 @@ class WebGateway {
|
|
|
8956
9174
|
}
|
|
8957
9175
|
}
|
|
8958
9176
|
|
|
9177
|
+
// packages/relay/src/gateway/desktop-stream-gateway.ts
|
|
9178
|
+
import {
|
|
9179
|
+
DESKTOP_BUFFERED_HARD_CLOSE_BYTES,
|
|
9180
|
+
DESKTOP_WS_MAX_PAYLOAD_BYTES
|
|
9181
|
+
} from "@ganglion/xacpx-relay-protocol";
|
|
9182
|
+
|
|
9183
|
+
// packages/relay/src/gateway/desktop-ticket-store.ts
|
|
9184
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
9185
|
+
import {
|
|
9186
|
+
DESKTOP_TICKET_TTL_MS,
|
|
9187
|
+
MAX_DESKTOP_STREAM_ID_LENGTH,
|
|
9188
|
+
MAX_DESKTOP_TICKET_LENGTH
|
|
9189
|
+
} from "@ganglion/xacpx-relay-protocol";
|
|
9190
|
+
function defaultMint() {
|
|
9191
|
+
return randomBytes2(32).toString("base64url");
|
|
9192
|
+
}
|
|
9193
|
+
|
|
9194
|
+
class DesktopTicketStore {
|
|
9195
|
+
tickets = new Map;
|
|
9196
|
+
now;
|
|
9197
|
+
mint;
|
|
9198
|
+
ttlMs;
|
|
9199
|
+
constructor(options = {}) {
|
|
9200
|
+
this.now = options.now ?? Date.now;
|
|
9201
|
+
this.mint = options.mint ?? defaultMint;
|
|
9202
|
+
this.ttlMs = options.ttlMs ?? DESKTOP_TICKET_TTL_MS;
|
|
9203
|
+
}
|
|
9204
|
+
mintTicket(input) {
|
|
9205
|
+
if (!input.streamId || input.streamId.length > MAX_DESKTOP_STREAM_ID_LENGTH) {
|
|
9206
|
+
throw new Error("desktop ticket requires a bounded streamId");
|
|
9207
|
+
}
|
|
9208
|
+
if (!input.accountId || !input.instanceId) {
|
|
9209
|
+
throw new Error("desktop ticket requires accountId and instanceId");
|
|
9210
|
+
}
|
|
9211
|
+
const issued = this.mint();
|
|
9212
|
+
const ticket = issued.length > MAX_DESKTOP_TICKET_LENGTH ? issued.slice(0, MAX_DESKTOP_TICKET_LENGTH) : issued;
|
|
9213
|
+
if (!ticket)
|
|
9214
|
+
throw new Error("desktop ticket mint produced an empty ticket");
|
|
9215
|
+
const record = {
|
|
9216
|
+
ticket,
|
|
9217
|
+
streamId: input.streamId,
|
|
9218
|
+
accountId: input.accountId,
|
|
9219
|
+
instanceId: input.instanceId,
|
|
9220
|
+
side: input.side,
|
|
9221
|
+
expiresAt: this.now() + this.ttlMs
|
|
9222
|
+
};
|
|
9223
|
+
const existing = this.tickets.get(ticket);
|
|
9224
|
+
if (existing && (existing.expiresAt > this.now() || existing.streamId !== record.streamId)) {
|
|
9225
|
+
throw new Error("desktop ticket collision");
|
|
9226
|
+
}
|
|
9227
|
+
this.tickets.set(ticket, record);
|
|
9228
|
+
return record;
|
|
9229
|
+
}
|
|
9230
|
+
consume(ticket, side, accountId) {
|
|
9231
|
+
const record = this.tickets.get(ticket);
|
|
9232
|
+
if (!record)
|
|
9233
|
+
return null;
|
|
9234
|
+
this.tickets.delete(ticket);
|
|
9235
|
+
if (record.side !== side)
|
|
9236
|
+
return null;
|
|
9237
|
+
if (record.expiresAt <= this.now())
|
|
9238
|
+
return null;
|
|
9239
|
+
if (accountId !== undefined && record.accountId !== accountId)
|
|
9240
|
+
return null;
|
|
9241
|
+
return record;
|
|
9242
|
+
}
|
|
9243
|
+
revokeForStream(streamId) {
|
|
9244
|
+
for (const [ticket, record] of [...this.tickets]) {
|
|
9245
|
+
if (record.streamId === streamId)
|
|
9246
|
+
this.tickets.delete(ticket);
|
|
9247
|
+
}
|
|
9248
|
+
}
|
|
9249
|
+
sweepExpired() {
|
|
9250
|
+
const now = this.now();
|
|
9251
|
+
let removed = 0;
|
|
9252
|
+
for (const [ticket, record] of [...this.tickets]) {
|
|
9253
|
+
if (record.expiresAt <= now) {
|
|
9254
|
+
this.tickets.delete(ticket);
|
|
9255
|
+
removed += 1;
|
|
9256
|
+
}
|
|
9257
|
+
}
|
|
9258
|
+
return removed;
|
|
9259
|
+
}
|
|
9260
|
+
size() {
|
|
9261
|
+
return this.tickets.size;
|
|
9262
|
+
}
|
|
9263
|
+
}
|
|
9264
|
+
|
|
9265
|
+
// packages/relay/src/gateway/desktop-stream-registry.ts
|
|
9266
|
+
import {
|
|
9267
|
+
DESKTOP_MAX_STREAMS_PER_ACCOUNT,
|
|
9268
|
+
DESKTOP_MAX_STREAMS_PER_INSTANCE
|
|
9269
|
+
} from "@ganglion/xacpx-relay-protocol";
|
|
9270
|
+
|
|
9271
|
+
class DesktopStreamRegistry {
|
|
9272
|
+
records = new Map;
|
|
9273
|
+
now;
|
|
9274
|
+
maxPerInstance;
|
|
9275
|
+
maxPerAccount;
|
|
9276
|
+
createStreamId;
|
|
9277
|
+
seq = 0;
|
|
9278
|
+
constructor(options = {}) {
|
|
9279
|
+
this.now = options.now ?? Date.now;
|
|
9280
|
+
this.maxPerInstance = options.maxStreamsPerInstance ?? DESKTOP_MAX_STREAMS_PER_INSTANCE;
|
|
9281
|
+
this.maxPerAccount = options.maxStreamsPerAccount ?? DESKTOP_MAX_STREAMS_PER_ACCOUNT;
|
|
9282
|
+
this.createStreamId = options.createStreamId ?? (() => `ds-${Date.now().toString(36)}-${(this.seq += 1).toString(36)}`);
|
|
9283
|
+
}
|
|
9284
|
+
reserve(input) {
|
|
9285
|
+
const now = this.now();
|
|
9286
|
+
this.pruneClosed();
|
|
9287
|
+
const liveForInstance = [...this.records.values()].filter((r) => r.instanceId === input.instanceId && r.accountId === input.accountId && r.state !== "closed");
|
|
9288
|
+
if (liveForInstance.length >= this.maxPerInstance) {
|
|
9289
|
+
return { ok: false, code: "desktop-busy", scope: "instance" };
|
|
9290
|
+
}
|
|
9291
|
+
const liveForAccount = [...this.records.values()].filter((r) => r.accountId === input.accountId && r.state !== "closed");
|
|
9292
|
+
if (liveForAccount.length >= this.maxPerAccount) {
|
|
9293
|
+
return { ok: false, code: "desktop-busy", scope: "account" };
|
|
9294
|
+
}
|
|
9295
|
+
const record = {
|
|
9296
|
+
streamId: this.createStreamId(),
|
|
9297
|
+
accountId: input.accountId,
|
|
9298
|
+
instanceId: input.instanceId,
|
|
9299
|
+
state: "preparing",
|
|
9300
|
+
createdAt: now,
|
|
9301
|
+
expiresAt: now + input.ttlMs
|
|
9302
|
+
};
|
|
9303
|
+
this.records.set(record.streamId, record);
|
|
9304
|
+
return { ok: true, record };
|
|
9305
|
+
}
|
|
9306
|
+
get(streamId) {
|
|
9307
|
+
return this.records.get(streamId);
|
|
9308
|
+
}
|
|
9309
|
+
setState(streamId, state) {
|
|
9310
|
+
const record = this.records.get(streamId);
|
|
9311
|
+
if (!record || record.state === "closed")
|
|
9312
|
+
return;
|
|
9313
|
+
record.state = state;
|
|
9314
|
+
return record;
|
|
9315
|
+
}
|
|
9316
|
+
extendExpiry(streamId, expiresAt) {
|
|
9317
|
+
const record = this.records.get(streamId);
|
|
9318
|
+
if (!record || record.state === "closed")
|
|
9319
|
+
return;
|
|
9320
|
+
if (expiresAt > record.expiresAt)
|
|
9321
|
+
record.expiresAt = expiresAt;
|
|
9322
|
+
return record;
|
|
9323
|
+
}
|
|
9324
|
+
isLive(record, now = this.now()) {
|
|
9325
|
+
return record !== undefined && record.state !== "closed" && record.expiresAt > now;
|
|
9326
|
+
}
|
|
9327
|
+
close(streamId) {
|
|
9328
|
+
const record = this.records.get(streamId);
|
|
9329
|
+
if (!record)
|
|
9330
|
+
return;
|
|
9331
|
+
record.state = "closed";
|
|
9332
|
+
return record;
|
|
9333
|
+
}
|
|
9334
|
+
closeForInstance(instanceId) {
|
|
9335
|
+
return [...this.records.values()].filter((r) => r.instanceId === instanceId && r.state !== "closed");
|
|
9336
|
+
}
|
|
9337
|
+
sweepExpired(now = this.now()) {
|
|
9338
|
+
return [...this.records.values()].filter((r) => (r.state === "preparing" || r.state === "waiting-browser") && r.expiresAt <= now);
|
|
9339
|
+
}
|
|
9340
|
+
pruneClosed() {
|
|
9341
|
+
for (const [id, record] of [...this.records]) {
|
|
9342
|
+
if (record.state === "closed")
|
|
9343
|
+
this.records.delete(id);
|
|
9344
|
+
}
|
|
9345
|
+
}
|
|
9346
|
+
}
|
|
9347
|
+
|
|
9348
|
+
// packages/relay/src/gateway/desktop-stream-gateway.ts
|
|
9349
|
+
function wireCloseReason(reason) {
|
|
9350
|
+
const bytes = Buffer.byteLength(reason, "utf8");
|
|
9351
|
+
if (bytes <= 64)
|
|
9352
|
+
return reason;
|
|
9353
|
+
return "stream-closed";
|
|
9354
|
+
}
|
|
9355
|
+
function wireCloseCode(reason) {
|
|
9356
|
+
return reason === "backpressure" ? 1013 : 1000;
|
|
9357
|
+
}
|
|
9358
|
+
var PRE_ATTACH_MAX_STREAMS = 64;
|
|
9359
|
+
var PRE_ATTACH_MAX_BYTES_PER_STREAM = 64 * 1024;
|
|
9360
|
+
|
|
9361
|
+
class DesktopStreamGateway {
|
|
9362
|
+
tickets;
|
|
9363
|
+
streams;
|
|
9364
|
+
paired = new Map;
|
|
9365
|
+
preAttach = new Map;
|
|
9366
|
+
logger;
|
|
9367
|
+
maxPayloadBytes;
|
|
9368
|
+
hardCloseBufferedBytes;
|
|
9369
|
+
onStreamClosed;
|
|
9370
|
+
constructor(options = {}) {
|
|
9371
|
+
this.tickets = options.tickets ?? new DesktopTicketStore;
|
|
9372
|
+
this.streams = options.streams ?? new DesktopStreamRegistry;
|
|
9373
|
+
this.logger = options.logger ?? createNoopRelayLogger();
|
|
9374
|
+
this.maxPayloadBytes = options.maxPayloadBytes ?? DESKTOP_WS_MAX_PAYLOAD_BYTES;
|
|
9375
|
+
this.hardCloseBufferedBytes = options.hardCloseBufferedBytes ?? DESKTOP_BUFFERED_HARD_CLOSE_BYTES;
|
|
9376
|
+
this.onStreamClosed = options.onStreamClosed;
|
|
9377
|
+
}
|
|
9378
|
+
get ticketStore() {
|
|
9379
|
+
return this.tickets;
|
|
9380
|
+
}
|
|
9381
|
+
get streamRegistry() {
|
|
9382
|
+
return this.streams;
|
|
9383
|
+
}
|
|
9384
|
+
reserve(input) {
|
|
9385
|
+
this.sweepExpired();
|
|
9386
|
+
return this.streams.reserve(input);
|
|
9387
|
+
}
|
|
9388
|
+
sweepExpired() {
|
|
9389
|
+
const expired = this.streams.sweepExpired();
|
|
9390
|
+
for (const record of expired) {
|
|
9391
|
+
this.closeStream(record.streamId, "stream-expired");
|
|
9392
|
+
}
|
|
9393
|
+
if (expired.length > 0)
|
|
9394
|
+
this.streams.pruneClosed();
|
|
9395
|
+
this.tickets.sweepExpired();
|
|
9396
|
+
return expired.length;
|
|
9397
|
+
}
|
|
9398
|
+
mintBrowserTicket(input) {
|
|
9399
|
+
const ticket = this.tickets.mintTicket({ ...input, side: "browser" });
|
|
9400
|
+
this.streams.extendExpiry(ticket.streamId, ticket.expiresAt);
|
|
9401
|
+
return ticket;
|
|
9402
|
+
}
|
|
9403
|
+
attachBrowser(ticket, socket, authenticatedAccountId) {
|
|
9404
|
+
const record = this.tickets.consume(ticket, "browser", authenticatedAccountId);
|
|
9405
|
+
if (!record)
|
|
9406
|
+
return this.reject(socket, "unknown-or-reused-ticket");
|
|
9407
|
+
return this.pair(record, "browser", socket);
|
|
9408
|
+
}
|
|
9409
|
+
precheckConnectorTicket(ticket) {
|
|
9410
|
+
const record = this.tickets.consume(ticket, "connector");
|
|
9411
|
+
if (!record)
|
|
9412
|
+
return { ok: false, reason: "unknown-or-reused-ticket" };
|
|
9413
|
+
const registryRecord = this.streams.get(record.streamId);
|
|
9414
|
+
if (!this.streams.isLive(registryRecord))
|
|
9415
|
+
return { ok: false, reason: "stream-expired" };
|
|
9416
|
+
if (registryRecord.accountId !== record.accountId || registryRecord.instanceId !== record.instanceId) {
|
|
9417
|
+
return { ok: false, reason: "ticket-identity-mismatch" };
|
|
9418
|
+
}
|
|
9419
|
+
return { ok: true, claim: { streamId: record.streamId, accountId: record.accountId, instanceId: record.instanceId } };
|
|
9420
|
+
}
|
|
9421
|
+
attachConnector(ticketOrClaim, socket) {
|
|
9422
|
+
if (typeof ticketOrClaim !== "string")
|
|
9423
|
+
return this.pair(ticketOrClaim, "connector", socket);
|
|
9424
|
+
const record = this.tickets.consume(ticketOrClaim, "connector");
|
|
9425
|
+
if (!record)
|
|
9426
|
+
return this.reject(socket, "unknown-or-reused-ticket");
|
|
9427
|
+
return this.pair(record, "connector", socket);
|
|
9428
|
+
}
|
|
9429
|
+
streamState(streamId) {
|
|
9430
|
+
const record = this.streams.get(streamId);
|
|
9431
|
+
return record?.state;
|
|
9432
|
+
}
|
|
9433
|
+
goActive(streamId, security) {
|
|
9434
|
+
const record = this.streams.get(streamId);
|
|
9435
|
+
if (!record || record.state === "closed")
|
|
9436
|
+
return;
|
|
9437
|
+
this.streams.setState(streamId, "active");
|
|
9438
|
+
if (!this.flushPreAttach(streamId))
|
|
9439
|
+
return;
|
|
9440
|
+
this.logger.info("relay.desktop.stream_active", "desktop stream active", { streamId, security });
|
|
9441
|
+
}
|
|
9442
|
+
reportConnectorReady(streamId, security) {
|
|
9443
|
+
const record = this.streams.get(streamId);
|
|
9444
|
+
if (!record || record.state === "closed")
|
|
9445
|
+
return false;
|
|
9446
|
+
if (security !== "vnc-auth")
|
|
9447
|
+
return false;
|
|
9448
|
+
const pair = this.paired.get(streamId) ?? {};
|
|
9449
|
+
pair.security = security;
|
|
9450
|
+
this.paired.set(streamId, pair);
|
|
9451
|
+
if (pair.browser && pair.connector)
|
|
9452
|
+
this.goActive(streamId, security);
|
|
9453
|
+
else
|
|
9454
|
+
this.streams.setState(streamId, "waiting-browser");
|
|
9455
|
+
return true;
|
|
9456
|
+
}
|
|
9457
|
+
closeStream(streamId, reason = "closed") {
|
|
9458
|
+
const pair = this.paired.get(streamId);
|
|
9459
|
+
const record = this.streams.get(streamId);
|
|
9460
|
+
const known = pair !== undefined || record !== undefined && record.state !== "closed";
|
|
9461
|
+
this.paired.delete(streamId);
|
|
9462
|
+
this.preAttach.delete(streamId);
|
|
9463
|
+
this.streams.close(streamId);
|
|
9464
|
+
this.tickets.revokeForStream(streamId);
|
|
9465
|
+
if (!known)
|
|
9466
|
+
return;
|
|
9467
|
+
this.logger.info("relay.desktop.stream_closed", "desktop stream closed", { streamId, reason });
|
|
9468
|
+
const wireReason = wireCloseReason(reason);
|
|
9469
|
+
const wireCode = wireCloseCode(reason);
|
|
9470
|
+
try {
|
|
9471
|
+
pair?.browser?.close(wireCode, wireReason);
|
|
9472
|
+
} catch {}
|
|
9473
|
+
try {
|
|
9474
|
+
pair?.connector?.close(wireCode, wireReason);
|
|
9475
|
+
} catch {}
|
|
9476
|
+
this.onStreamClosed?.(streamId);
|
|
9477
|
+
}
|
|
9478
|
+
closeForInstance(instanceId, reason = "instance-offline") {
|
|
9479
|
+
for (const record of this.streams.closeForInstance(instanceId)) {
|
|
9480
|
+
this.closeStream(record.streamId, reason);
|
|
9481
|
+
}
|
|
9482
|
+
}
|
|
9483
|
+
pair(record, side, socket) {
|
|
9484
|
+
const streamId = record.streamId;
|
|
9485
|
+
const registryRecord = this.streams.get(streamId);
|
|
9486
|
+
if (!this.streams.isLive(registryRecord))
|
|
9487
|
+
return this.reject(socket, "stream-expired");
|
|
9488
|
+
if (registryRecord.accountId !== record.accountId || registryRecord.instanceId !== record.instanceId) {
|
|
9489
|
+
return this.reject(socket, "ticket-identity-mismatch");
|
|
9490
|
+
}
|
|
9491
|
+
const pair = this.paired.get(streamId) ?? {};
|
|
9492
|
+
if (pair[side])
|
|
9493
|
+
return this.reject(socket, "side-already-attached");
|
|
9494
|
+
pair[side] = socket;
|
|
9495
|
+
this.paired.set(streamId, pair);
|
|
9496
|
+
socket.on("message", (data, isBinary) => this.onFrame(streamId, side, socket, data, isBinary));
|
|
9497
|
+
socket.on("close", () => this.closeStream(streamId, `${side}-close`));
|
|
9498
|
+
if (pair.browser && pair.connector && pair.security) {
|
|
9499
|
+
this.goActive(streamId, pair.security);
|
|
9500
|
+
}
|
|
9501
|
+
return { ok: true, streamId };
|
|
9502
|
+
}
|
|
9503
|
+
onFrame(streamId, from, socket, data, isBinary) {
|
|
9504
|
+
const record = this.streams.get(streamId);
|
|
9505
|
+
const pair = this.paired.get(streamId);
|
|
9506
|
+
if (!record || record.state === "closed" || !pair) {
|
|
9507
|
+
return;
|
|
9508
|
+
}
|
|
9509
|
+
if (!isBinary || !(data instanceof Uint8Array)) {
|
|
9510
|
+
this.logger.warn("relay.desktop.text_frame", "desktop text frame rejected", { streamId });
|
|
9511
|
+
this.closeStream(streamId, "text-frame");
|
|
9512
|
+
return;
|
|
9513
|
+
}
|
|
9514
|
+
if (data.byteLength === 0 || data.byteLength > this.maxPayloadBytes) {
|
|
9515
|
+
this.logger.warn("relay.desktop.oversize_frame", "desktop oversize frame rejected", {
|
|
9516
|
+
streamId,
|
|
9517
|
+
bytes: data.byteLength
|
|
9518
|
+
});
|
|
9519
|
+
this.closeStream(streamId, "oversize-frame");
|
|
9520
|
+
return;
|
|
9521
|
+
}
|
|
9522
|
+
const peer = from === "browser" ? pair.connector : pair.browser;
|
|
9523
|
+
if (!peer) {
|
|
9524
|
+
this.bufferPreAttach(streamId, data);
|
|
9525
|
+
return;
|
|
9526
|
+
}
|
|
9527
|
+
if (peer.bufferedAmount > this.hardCloseBufferedBytes || socket.bufferedAmount > this.hardCloseBufferedBytes) {
|
|
9528
|
+
this.logger.warn("relay.desktop.backpressure_close", "slow desktop peer evicted", { streamId });
|
|
9529
|
+
this.closeStream(streamId, "backpressure");
|
|
9530
|
+
return;
|
|
9531
|
+
}
|
|
9532
|
+
try {
|
|
9533
|
+
peer.send(data);
|
|
9534
|
+
} catch {
|
|
9535
|
+
this.closeStream(streamId, "send-failed");
|
|
9536
|
+
}
|
|
9537
|
+
}
|
|
9538
|
+
reject(socket, reason) {
|
|
9539
|
+
try {
|
|
9540
|
+
socket.close(4403, reason);
|
|
9541
|
+
} catch {}
|
|
9542
|
+
return { ok: false, reason };
|
|
9543
|
+
}
|
|
9544
|
+
bufferPreAttach(streamId, data) {
|
|
9545
|
+
const existing = this.preAttach.get(streamId);
|
|
9546
|
+
const bytes = (existing?.bytes ?? 0) + data.byteLength;
|
|
9547
|
+
if (bytes > PRE_ATTACH_MAX_BYTES_PER_STREAM) {
|
|
9548
|
+
this.logger.warn("relay.desktop.preattach_overflow", "pre-attach desktop buffer overflow", { streamId });
|
|
9549
|
+
this.closeStream(streamId, "preattach-overflow");
|
|
9550
|
+
return;
|
|
9551
|
+
}
|
|
9552
|
+
if (!existing && this.preAttach.size >= PRE_ATTACH_MAX_STREAMS) {
|
|
9553
|
+
this.logger.warn("relay.desktop.preattach_overflow", "too many pre-attach desktop streams", { streamId });
|
|
9554
|
+
this.closeStream(streamId, "preattach-overflow");
|
|
9555
|
+
return;
|
|
9556
|
+
}
|
|
9557
|
+
const entry = existing ?? { chunks: [], bytes: 0 };
|
|
9558
|
+
entry.chunks.push(data);
|
|
9559
|
+
entry.bytes = bytes;
|
|
9560
|
+
this.preAttach.set(streamId, entry);
|
|
9561
|
+
}
|
|
9562
|
+
flushPreAttach(streamId) {
|
|
9563
|
+
const entry = this.preAttach.get(streamId);
|
|
9564
|
+
if (!entry)
|
|
9565
|
+
return true;
|
|
9566
|
+
this.preAttach.delete(streamId);
|
|
9567
|
+
const pair = this.paired.get(streamId);
|
|
9568
|
+
const peer = pair?.browser;
|
|
9569
|
+
if (!peer || pair?.connector === undefined)
|
|
9570
|
+
return true;
|
|
9571
|
+
for (const chunk of entry.chunks) {
|
|
9572
|
+
try {
|
|
9573
|
+
peer.send(chunk);
|
|
9574
|
+
} catch {
|
|
9575
|
+
this.closeStream(streamId, "send-failed");
|
|
9576
|
+
return false;
|
|
9577
|
+
}
|
|
9578
|
+
}
|
|
9579
|
+
return true;
|
|
9580
|
+
}
|
|
9581
|
+
}
|
|
9582
|
+
|
|
9583
|
+
// packages/relay/src/gateway/desktop-viewer-cancel.ts
|
|
9584
|
+
import { MSG as MSG2 } from "@ganglion/xacpx-relay-protocol";
|
|
9585
|
+
function sendDesktopCancel(gateway, instanceId, streamId) {
|
|
9586
|
+
try {
|
|
9587
|
+
gateway?.sendEvent(instanceId, MSG2.desktopCancel, { streamId });
|
|
9588
|
+
} catch {}
|
|
9589
|
+
}
|
|
9590
|
+
function cancelViewerDesktopStreamByRequest(gateway, desktop, owners, requestId, viewerId, reason = "viewer-disconnected") {
|
|
9591
|
+
for (const [streamId, owner] of [...owners]) {
|
|
9592
|
+
if (owner.requestId !== requestId || owner.viewerId !== viewerId)
|
|
9593
|
+
continue;
|
|
9594
|
+
const state = desktop.streamState(streamId);
|
|
9595
|
+
if (state !== "preparing" && state !== "waiting-browser")
|
|
9596
|
+
continue;
|
|
9597
|
+
sendDesktopCancel(gateway, owner.instanceId, streamId);
|
|
9598
|
+
owners.delete(streamId);
|
|
9599
|
+
desktop.closeStream(streamId, reason);
|
|
9600
|
+
return true;
|
|
9601
|
+
}
|
|
9602
|
+
return false;
|
|
9603
|
+
}
|
|
9604
|
+
function cancelViewerDesktopStreams(gateway, desktop, owners, viewerId) {
|
|
9605
|
+
for (const [streamId, owner] of [...owners]) {
|
|
9606
|
+
if (owner.viewerId !== viewerId)
|
|
9607
|
+
continue;
|
|
9608
|
+
sendDesktopCancel(gateway, owner.instanceId, streamId);
|
|
9609
|
+
owners.delete(streamId);
|
|
9610
|
+
desktop.closeStream(streamId, "viewer-disconnected");
|
|
9611
|
+
}
|
|
9612
|
+
}
|
|
9613
|
+
|
|
8959
9614
|
// packages/relay/src/push.ts
|
|
8960
9615
|
var import_web_push = __toESM(require_src2(), 1);
|
|
8961
9616
|
import { createECDH } from "node:crypto";
|
|
@@ -9190,7 +9845,9 @@ class PushSubscriptionStore {
|
|
|
9190
9845
|
import {
|
|
9191
9846
|
decodeEnvelope as decodeEnvelope2,
|
|
9192
9847
|
isErrorPayload,
|
|
9193
|
-
|
|
9848
|
+
DESKTOP_HUB_REQUEST_TIMEOUT_MS,
|
|
9849
|
+
MAX_DESKTOP_ERROR_MESSAGE_LENGTH,
|
|
9850
|
+
MSG as MSG3,
|
|
9194
9851
|
parseTerminalEventPayload,
|
|
9195
9852
|
parseWebClientMessage
|
|
9196
9853
|
} from "@ganglion/xacpx-relay-protocol";
|
|
@@ -9203,6 +9860,15 @@ function fail(deps, socket, requestId, instanceId, code, message) {
|
|
|
9203
9860
|
message
|
|
9204
9861
|
});
|
|
9205
9862
|
}
|
|
9863
|
+
function failDesktop(deps, socket, requestId, instanceId, code, message) {
|
|
9864
|
+
deps.webGateway.send(socket, {
|
|
9865
|
+
kind: "desktop-request-failed",
|
|
9866
|
+
requestId,
|
|
9867
|
+
instanceId,
|
|
9868
|
+
code,
|
|
9869
|
+
message
|
|
9870
|
+
});
|
|
9871
|
+
}
|
|
9206
9872
|
function mapConnectorError(err) {
|
|
9207
9873
|
if (err instanceof Error) {
|
|
9208
9874
|
if (err.message === "instance-offline") {
|
|
@@ -9218,16 +9884,24 @@ function mapConnectorError(err) {
|
|
|
9218
9884
|
}
|
|
9219
9885
|
return { code: "terminal-protocol-error", message: String(err) };
|
|
9220
9886
|
}
|
|
9221
|
-
|
|
9887
|
+
function mapDesktopConnectorError(err) {
|
|
9888
|
+
if (err instanceof Error) {
|
|
9889
|
+
if (err.message === "instance-offline" || err.message === "instance-reconnected") {
|
|
9890
|
+
return { code: "desktop-instance-offline", message: "instance is offline" };
|
|
9891
|
+
}
|
|
9892
|
+
if (err.message === "timeout") {
|
|
9893
|
+
return { code: "desktop-stream-timeout", message: "desktop prepare timed out" };
|
|
9894
|
+
}
|
|
9895
|
+
return { code: "desktop-protocol-error", message: err.message.slice(0, 160) };
|
|
9896
|
+
}
|
|
9897
|
+
return { code: "desktop-protocol-error", message: String(err).slice(0, 160) };
|
|
9898
|
+
}
|
|
9899
|
+
async function sendConnectorRequest(deps, instanceId, type, payload, timeoutMs = TERMINAL_REQUEST_TIMEOUT_MS) {
|
|
9222
9900
|
try {
|
|
9223
|
-
return await deps.gateway.sendRequest(instanceId, type, payload, {
|
|
9224
|
-
timeoutMs: TERMINAL_REQUEST_TIMEOUT_MS
|
|
9225
|
-
});
|
|
9901
|
+
return await deps.gateway.sendRequest(instanceId, type, payload, { timeoutMs });
|
|
9226
9902
|
} catch (err) {
|
|
9227
9903
|
if (err instanceof Error && err.message === "instance-reconnected" && deps.gateway.isOnline(instanceId)) {
|
|
9228
|
-
return await deps.gateway.sendRequest(instanceId, type, payload, {
|
|
9229
|
-
timeoutMs: TERMINAL_REQUEST_TIMEOUT_MS
|
|
9230
|
-
});
|
|
9904
|
+
return await deps.gateway.sendRequest(instanceId, type, payload, { timeoutMs });
|
|
9231
9905
|
}
|
|
9232
9906
|
throw err;
|
|
9233
9907
|
}
|
|
@@ -9263,6 +9937,35 @@ async function handleWebClientMessageAsync(deps, accountId, socket, raw) {
|
|
|
9263
9937
|
instanceId,
|
|
9264
9938
|
...deps.stateSnapshot(instanceId)
|
|
9265
9939
|
});
|
|
9940
|
+
const open = (deps.interactions?.listForInstance(instanceId) ?? []).filter((entry) => entry.expiresAt > Date.now());
|
|
9941
|
+
const frames = open.map((entry) => ({
|
|
9942
|
+
chatKey: entry.chatKey,
|
|
9943
|
+
sessionAlias: entry.sessionAlias,
|
|
9944
|
+
instanceId,
|
|
9945
|
+
interaction: {
|
|
9946
|
+
requestId: entry.requestId,
|
|
9947
|
+
kind: entry.kind,
|
|
9948
|
+
...entry.conversation !== undefined ? { conversation: entry.conversation } : {},
|
|
9949
|
+
expiresAt: entry.expiresAt,
|
|
9950
|
+
...entry.kind === "elicitation" ? { elicitation: entry.elicitation } : { permission: entry.permission }
|
|
9951
|
+
}
|
|
9952
|
+
}));
|
|
9953
|
+
for (const frame of frames) {
|
|
9954
|
+
deps.webGateway.send(socket, {
|
|
9955
|
+
kind: "control-event",
|
|
9956
|
+
instanceId,
|
|
9957
|
+
event: { type: "interaction-opened", ...frame }
|
|
9958
|
+
});
|
|
9959
|
+
}
|
|
9960
|
+
deps.webGateway.send(socket, {
|
|
9961
|
+
kind: "interaction-snapshot",
|
|
9962
|
+
instanceId,
|
|
9963
|
+
interactions: frames.map((frame) => ({
|
|
9964
|
+
chatKey: frame.chatKey,
|
|
9965
|
+
sessionAlias: frame.sessionAlias,
|
|
9966
|
+
interaction: frame.interaction
|
|
9967
|
+
}))
|
|
9968
|
+
});
|
|
9266
9969
|
}
|
|
9267
9970
|
return;
|
|
9268
9971
|
}
|
|
@@ -9276,12 +9979,20 @@ async function handleWebClientMessageAsync(deps, accountId, socket, raw) {
|
|
|
9276
9979
|
const viewerId = deps.webGateway.getViewerId(socket);
|
|
9277
9980
|
if (!viewerId || !deps.webGateway.socketOwnsAttachment(socket, msg.attachmentId))
|
|
9278
9981
|
return;
|
|
9279
|
-
deps.gateway.sendEvent(msg.instanceId,
|
|
9982
|
+
deps.gateway.sendEvent(msg.instanceId, MSG3.terminalStreamStart, {
|
|
9280
9983
|
attachmentId: msg.attachmentId,
|
|
9281
9984
|
viewerId
|
|
9282
9985
|
});
|
|
9283
9986
|
return;
|
|
9284
9987
|
}
|
|
9988
|
+
if (msg.kind === "desktop-open") {
|
|
9989
|
+
await handleDesktopOpen(deps, accountId, socket, msg);
|
|
9990
|
+
return;
|
|
9991
|
+
}
|
|
9992
|
+
if (msg.kind === "desktop-close") {
|
|
9993
|
+
handleDesktopClose(deps, accountId, socket, msg);
|
|
9994
|
+
return;
|
|
9995
|
+
}
|
|
9285
9996
|
if (msg.kind === "terminal-take-control") {
|
|
9286
9997
|
await handleTakeControl(deps, socket, msg);
|
|
9287
9998
|
return;
|
|
@@ -9298,7 +10009,7 @@ async function handleWebClientMessageAsync(deps, accountId, socket, raw) {
|
|
|
9298
10009
|
const viewerId = deps.webGateway.getViewerId(socket);
|
|
9299
10010
|
if (!viewerId || !deps.webGateway.socketOwnsAttachment(socket, msg.attachmentId))
|
|
9300
10011
|
return;
|
|
9301
|
-
deps.gateway.sendEvent(msg.instanceId,
|
|
10012
|
+
deps.gateway.sendEvent(msg.instanceId, MSG3.terminalInput, {
|
|
9302
10013
|
attachmentId: msg.attachmentId,
|
|
9303
10014
|
generation: msg.generation,
|
|
9304
10015
|
viewerId,
|
|
@@ -9310,7 +10021,7 @@ async function handleWebClientMessageAsync(deps, accountId, socket, raw) {
|
|
|
9310
10021
|
const viewerId = deps.webGateway.getViewerId(socket);
|
|
9311
10022
|
if (!viewerId || !deps.webGateway.socketOwnsAttachment(socket, msg.attachmentId))
|
|
9312
10023
|
return;
|
|
9313
|
-
deps.gateway.sendEvent(msg.instanceId,
|
|
10024
|
+
deps.gateway.sendEvent(msg.instanceId, MSG3.terminalResize, {
|
|
9314
10025
|
attachmentId: msg.attachmentId,
|
|
9315
10026
|
generation: msg.generation,
|
|
9316
10027
|
viewerId,
|
|
@@ -9323,7 +10034,7 @@ async function handleWebClientMessageAsync(deps, accountId, socket, raw) {
|
|
|
9323
10034
|
const viewerId = deps.webGateway.getViewerId(socket);
|
|
9324
10035
|
if (!viewerId || !deps.webGateway.socketOwnsAttachment(socket, msg.attachmentId))
|
|
9325
10036
|
return;
|
|
9326
|
-
deps.gateway.sendEvent(msg.instanceId,
|
|
10037
|
+
deps.gateway.sendEvent(msg.instanceId, MSG3.terminalHeartbeat, {
|
|
9327
10038
|
attachmentId: msg.attachmentId,
|
|
9328
10039
|
viewerId
|
|
9329
10040
|
});
|
|
@@ -9334,7 +10045,7 @@ async function handleWebClientMessageAsync(deps, accountId, socket, raw) {
|
|
|
9334
10045
|
return;
|
|
9335
10046
|
const binding = deps.webGateway.unbindAttachment(msg.attachmentId);
|
|
9336
10047
|
if (binding) {
|
|
9337
|
-
deps.gateway.sendEvent(msg.instanceId,
|
|
10048
|
+
deps.gateway.sendEvent(msg.instanceId, MSG3.terminalDetach, {
|
|
9338
10049
|
attachmentId: msg.attachmentId,
|
|
9339
10050
|
viewerId: binding.viewerId
|
|
9340
10051
|
});
|
|
@@ -9342,18 +10053,18 @@ async function handleWebClientMessageAsync(deps, accountId, socket, raw) {
|
|
|
9342
10053
|
return;
|
|
9343
10054
|
}
|
|
9344
10055
|
if (msg.kind === "terminal-input" && "terminalId" in msg) {
|
|
9345
|
-
deps.gateway.sendEvent(msg.instanceId,
|
|
10056
|
+
deps.gateway.sendEvent(msg.instanceId, MSG3.terminalInput, {
|
|
9346
10057
|
terminalId: msg.terminalId,
|
|
9347
10058
|
data: msg.data
|
|
9348
10059
|
});
|
|
9349
10060
|
} else if (msg.kind === "terminal-resize" && "terminalId" in msg) {
|
|
9350
|
-
deps.gateway.sendEvent(msg.instanceId,
|
|
10061
|
+
deps.gateway.sendEvent(msg.instanceId, MSG3.terminalResize, {
|
|
9351
10062
|
terminalId: msg.terminalId,
|
|
9352
10063
|
cols: msg.cols,
|
|
9353
10064
|
rows: msg.rows
|
|
9354
10065
|
});
|
|
9355
10066
|
} else if (msg.kind === "terminal-close") {
|
|
9356
|
-
deps.gateway.sendEvent(msg.instanceId,
|
|
10067
|
+
deps.gateway.sendEvent(msg.instanceId, MSG3.terminalClose, { terminalId: msg.terminalId });
|
|
9357
10068
|
}
|
|
9358
10069
|
}
|
|
9359
10070
|
async function handleTerminalOpen(deps, accountId, socket, msg) {
|
|
@@ -9368,7 +10079,7 @@ async function handleTerminalOpen(deps, accountId, socket, msg) {
|
|
|
9368
10079
|
}
|
|
9369
10080
|
let payload;
|
|
9370
10081
|
try {
|
|
9371
|
-
payload = await sendConnectorRequest(deps, msg.instanceId,
|
|
10082
|
+
payload = await sendConnectorRequest(deps, msg.instanceId, MSG3.terminalOpen, {
|
|
9372
10083
|
chatKey: `relay:${accountId}`,
|
|
9373
10084
|
sessionAlias: msg.sessionAlias,
|
|
9374
10085
|
viewerId,
|
|
@@ -9421,8 +10132,113 @@ async function handleTerminalOpen(deps, accountId, socket, msg) {
|
|
|
9421
10132
|
detachConnectorAttachment(deps, msg.instanceId, result.attachmentId, viewerId);
|
|
9422
10133
|
}
|
|
9423
10134
|
}
|
|
10135
|
+
async function handleDesktopOpen(deps, accountId, socket, msg) {
|
|
10136
|
+
if (!deps.desktop) {
|
|
10137
|
+
failDesktop(deps, socket, msg.requestId, msg.instanceId, "desktop-protocol-error", "desktop is not enabled on this hub");
|
|
10138
|
+
return;
|
|
10139
|
+
}
|
|
10140
|
+
const ownerViewerId = deps.webGateway.getViewerId(socket);
|
|
10141
|
+
if (!ownerViewerId) {
|
|
10142
|
+
failDesktop(deps, socket, msg.requestId, msg.instanceId, "desktop-protocol-error", "missing viewer identity");
|
|
10143
|
+
return;
|
|
10144
|
+
}
|
|
10145
|
+
const owned = deps.instances.getOwned(msg.instanceId, accountId);
|
|
10146
|
+
if (!owned)
|
|
10147
|
+
return;
|
|
10148
|
+
if (!deps.gateway.isOnline(msg.instanceId)) {
|
|
10149
|
+
failDesktop(deps, socket, msg.requestId, msg.instanceId, "desktop-instance-offline", "instance is offline");
|
|
10150
|
+
return;
|
|
10151
|
+
}
|
|
10152
|
+
if (!owned.capabilities?.includes("desktop.rfb.v1")) {
|
|
10153
|
+
failDesktop(deps, socket, msg.requestId, msg.instanceId, "desktop-disabled", "desktop is not enabled on this instance");
|
|
10154
|
+
return;
|
|
10155
|
+
}
|
|
10156
|
+
const reserved = deps.desktop.reserve(accountId, msg.instanceId);
|
|
10157
|
+
if (!reserved.ok) {
|
|
10158
|
+
failDesktop(deps, socket, msg.requestId, msg.instanceId, reserved.code, reserved.scope === "account" ? "too many active desktop viewers on this account" : "another desktop viewer is active");
|
|
10159
|
+
return;
|
|
10160
|
+
}
|
|
10161
|
+
const streamId = reserved.streamId;
|
|
10162
|
+
deps.desktop.trackOwner(streamId, { viewerId: ownerViewerId, accountId, instanceId: msg.instanceId, requestId: msg.requestId });
|
|
10163
|
+
const cancelStream = (reason) => {
|
|
10164
|
+
sendDesktopCancel(deps.gateway, msg.instanceId, streamId);
|
|
10165
|
+
deps.desktop?.cancel(streamId, reason);
|
|
10166
|
+
};
|
|
10167
|
+
const failWith = (code, message) => {
|
|
10168
|
+
cancelStream(code);
|
|
10169
|
+
failDesktop(deps, socket, msg.requestId, msg.instanceId, code, message);
|
|
10170
|
+
};
|
|
10171
|
+
const connectorTicket = deps.desktop.mintConnectorTicket(streamId, accountId, msg.instanceId);
|
|
10172
|
+
let payload;
|
|
10173
|
+
try {
|
|
10174
|
+
payload = await deps.gateway.sendRequest(msg.instanceId, MSG3.desktopPrepare, {
|
|
10175
|
+
streamId,
|
|
10176
|
+
ticket: connectorTicket.ticket,
|
|
10177
|
+
expiresAt: connectorTicket.expiresAt
|
|
10178
|
+
}, { timeoutMs: DESKTOP_HUB_REQUEST_TIMEOUT_MS });
|
|
10179
|
+
} catch (err) {
|
|
10180
|
+
const mapped = mapDesktopConnectorError(err);
|
|
10181
|
+
failWith(mapped.code, mapped.message);
|
|
10182
|
+
return;
|
|
10183
|
+
}
|
|
10184
|
+
if (isErrorPayload(payload)) {
|
|
10185
|
+
const code = payload.error.code.length > 0 ? payload.error.code.slice(0, 128) : "desktop-protocol-error";
|
|
10186
|
+
failWith(code, payload.error.message.slice(0, MAX_DESKTOP_ERROR_MESSAGE_LENGTH));
|
|
10187
|
+
return;
|
|
10188
|
+
}
|
|
10189
|
+
const result = payload;
|
|
10190
|
+
if (!result || result.streamId !== streamId || result.security !== "vnc-auth" && result.security !== "ard") {
|
|
10191
|
+
failWith("desktop-protocol-error", "malformed prepare result");
|
|
10192
|
+
return;
|
|
10193
|
+
}
|
|
10194
|
+
if (result.security !== "vnc-auth") {
|
|
10195
|
+
failWith("desktop-auth-unsupported", "Apple Remote Desktop auth needs Phase B");
|
|
10196
|
+
return;
|
|
10197
|
+
}
|
|
10198
|
+
if (deps.webGateway.getViewerId(socket) !== ownerViewerId || !deps.desktop.ownsStream(streamId, ownerViewerId)) {
|
|
10199
|
+
failWith("desktop-stream-timeout", "requesting viewer disconnected");
|
|
10200
|
+
return;
|
|
10201
|
+
}
|
|
10202
|
+
if (!deps.desktop.markReady(streamId, result.security)) {
|
|
10203
|
+
failWith("desktop-stream-timeout", "desktop stream expired");
|
|
10204
|
+
return;
|
|
10205
|
+
}
|
|
10206
|
+
const browserTicket = deps.desktop.mintBrowserTicket(streamId, accountId, msg.instanceId);
|
|
10207
|
+
const sent = deps.webGateway.send(socket, {
|
|
10208
|
+
kind: "desktop-opened",
|
|
10209
|
+
requestId: msg.requestId,
|
|
10210
|
+
instanceId: msg.instanceId,
|
|
10211
|
+
streamId,
|
|
10212
|
+
wsPath: `/desktop/observe?ticket=${browserTicket.ticket}`,
|
|
10213
|
+
expiresAt: browserTicket.expiresAt,
|
|
10214
|
+
security: result.security
|
|
10215
|
+
});
|
|
10216
|
+
if (!sent) {
|
|
10217
|
+
failWith("desktop-stream-timeout", "requesting viewer disconnected");
|
|
10218
|
+
return;
|
|
10219
|
+
}
|
|
10220
|
+
}
|
|
10221
|
+
function handleDesktopClose(deps, accountId, socket, msg) {
|
|
10222
|
+
if (!deps.desktop)
|
|
10223
|
+
return;
|
|
10224
|
+
const viewerId = deps.webGateway.getViewerId(socket) ?? "";
|
|
10225
|
+
if (msg.streamId !== undefined) {
|
|
10226
|
+
const owner = deps.desktop.streamOwner(msg.streamId);
|
|
10227
|
+
if (!owner || owner.viewerId !== viewerId)
|
|
10228
|
+
return;
|
|
10229
|
+
if (owner.accountId !== accountId)
|
|
10230
|
+
return;
|
|
10231
|
+
sendDesktopCancel(deps.gateway, owner.instanceId, msg.streamId);
|
|
10232
|
+
deps.desktop.cancel(msg.streamId, "browser-close");
|
|
10233
|
+
return;
|
|
10234
|
+
}
|
|
10235
|
+
if (msg.requestId === undefined)
|
|
10236
|
+
return;
|
|
10237
|
+
if (!deps.desktop.cancelPendingByRequest(msg.requestId, viewerId, "browser-close"))
|
|
10238
|
+
return;
|
|
10239
|
+
}
|
|
9424
10240
|
function detachConnectorAttachment(deps, instanceId, attachmentId, viewerId) {
|
|
9425
|
-
deps.gateway.sendEvent(instanceId,
|
|
10241
|
+
deps.gateway.sendEvent(instanceId, MSG3.terminalDetach, {
|
|
9426
10242
|
attachmentId,
|
|
9427
10243
|
viewerId
|
|
9428
10244
|
});
|
|
@@ -9434,7 +10250,7 @@ async function handleTakeControl(deps, socket, msg) {
|
|
|
9434
10250
|
return;
|
|
9435
10251
|
}
|
|
9436
10252
|
try {
|
|
9437
|
-
const payload = await sendConnectorRequest(deps, msg.instanceId,
|
|
10253
|
+
const payload = await sendConnectorRequest(deps, msg.instanceId, MSG3.terminalTakeControl, { attachmentId: msg.attachmentId, generation: msg.generation, viewerId });
|
|
9438
10254
|
if (isErrorPayload(payload)) {
|
|
9439
10255
|
fail(deps, socket, msg.requestId, msg.instanceId, payload.error.code, payload.error.message);
|
|
9440
10256
|
return;
|
|
@@ -9462,7 +10278,7 @@ async function handleResync(deps, socket, msg) {
|
|
|
9462
10278
|
return;
|
|
9463
10279
|
}
|
|
9464
10280
|
try {
|
|
9465
|
-
const payload = await sendConnectorRequest(deps, msg.instanceId,
|
|
10281
|
+
const payload = await sendConnectorRequest(deps, msg.instanceId, MSG3.terminalResync, { attachmentId: msg.attachmentId, generation: msg.generation, viewerId });
|
|
9466
10282
|
if (isErrorPayload(payload)) {
|
|
9467
10283
|
fail(deps, socket, msg.requestId, msg.instanceId, payload.error.code, payload.error.message);
|
|
9468
10284
|
return;
|
|
@@ -9481,7 +10297,7 @@ async function handleResync(deps, socket, msg) {
|
|
|
9481
10297
|
}
|
|
9482
10298
|
async function handleTerminate(deps, socket, msg) {
|
|
9483
10299
|
try {
|
|
9484
|
-
const payload = await sendConnectorRequest(deps, msg.instanceId,
|
|
10300
|
+
const payload = await sendConnectorRequest(deps, msg.instanceId, MSG3.terminalTerminate, { terminalId: msg.terminalId, generation: msg.generation });
|
|
9485
10301
|
if (isErrorPayload(payload)) {
|
|
9486
10302
|
fail(deps, socket, msg.requestId, msg.instanceId, payload.error.code, payload.error.message);
|
|
9487
10303
|
return;
|
|
@@ -9507,8 +10323,8 @@ async function handleTerminate(deps, socket, msg) {
|
|
|
9507
10323
|
}
|
|
9508
10324
|
}
|
|
9509
10325
|
function handleConnectorTerminalEvent(webGateway, instanceId, envelopeType, payload) {
|
|
9510
|
-
if (envelopeType ===
|
|
9511
|
-
const parsed = parseTerminalEventPayload(
|
|
10326
|
+
if (envelopeType === MSG3.terminalViewerEvent) {
|
|
10327
|
+
const parsed = parseTerminalEventPayload(MSG3.terminalViewerEvent, payload);
|
|
9512
10328
|
if (!parsed)
|
|
9513
10329
|
return true;
|
|
9514
10330
|
const p = parsed;
|
|
@@ -9599,8 +10415,8 @@ function handleConnectorTerminalEvent(webGateway, instanceId, envelopeType, payl
|
|
|
9599
10415
|
webGateway.sendToAttachment(p.viewerId, p.attachmentId, event);
|
|
9600
10416
|
return true;
|
|
9601
10417
|
}
|
|
9602
|
-
if (envelopeType ===
|
|
9603
|
-
const parsed = parseTerminalEventPayload(
|
|
10418
|
+
if (envelopeType === MSG3.terminalResourceExit) {
|
|
10419
|
+
const parsed = parseTerminalEventPayload(MSG3.terminalResourceExit, payload);
|
|
9604
10420
|
if (!parsed)
|
|
9605
10421
|
return true;
|
|
9606
10422
|
const p = parsed;
|
|
@@ -9626,7 +10442,7 @@ import { serveStatic } from "@hono/node-server/serve-static";
|
|
|
9626
10442
|
import { createECDH as createECDH2, randomUUID as randomUUID4 } from "node:crypto";
|
|
9627
10443
|
import {
|
|
9628
10444
|
isErrorPayload as isErrorPayload2,
|
|
9629
|
-
MSG as
|
|
10445
|
+
MSG as MSG4,
|
|
9630
10446
|
parseControlPayload
|
|
9631
10447
|
} from "@ganglion/xacpx-relay-protocol";
|
|
9632
10448
|
|
|
@@ -9698,12 +10514,18 @@ function compactDetail(detail, isSubagent) {
|
|
|
9698
10514
|
return {
|
|
9699
10515
|
type: "command",
|
|
9700
10516
|
command: detail.command,
|
|
9701
|
-
...detail.exitCode !== undefined ? { exitCode: detail.exitCode } : {}
|
|
10517
|
+
...detail.exitCode !== undefined ? { exitCode: detail.exitCode } : {},
|
|
10518
|
+
...detail.truncated !== undefined ? { truncated: detail.truncated } : {}
|
|
9702
10519
|
};
|
|
9703
10520
|
case "read":
|
|
9704
10521
|
return { type: "read", path: detail.path, ...detail.lines ? { lines: detail.lines } : {} };
|
|
9705
10522
|
case "search":
|
|
9706
|
-
return {
|
|
10523
|
+
return {
|
|
10524
|
+
type: "search",
|
|
10525
|
+
query: detail.query,
|
|
10526
|
+
...detail.count !== undefined ? { count: detail.count } : {},
|
|
10527
|
+
...detail.truncated !== undefined ? { truncated: detail.truncated } : {}
|
|
10528
|
+
};
|
|
9707
10529
|
case "text":
|
|
9708
10530
|
return { type: "text", text: clip(detail.text, COMPACT_DETAIL_PREVIEW) };
|
|
9709
10531
|
case "fields":
|
|
@@ -9878,25 +10700,25 @@ var LOGIN_FAILURES_SWEEP_AT = 1024;
|
|
|
9878
10700
|
var LOGIN_FAILURES_MAX = 4096;
|
|
9879
10701
|
var GLOBAL_MAX_FAILURES = 200;
|
|
9880
10702
|
var CHAT_SCOPED_TYPES = new Set([
|
|
9881
|
-
|
|
9882
|
-
|
|
9883
|
-
|
|
9884
|
-
|
|
9885
|
-
|
|
9886
|
-
|
|
9887
|
-
|
|
9888
|
-
|
|
9889
|
-
|
|
9890
|
-
|
|
9891
|
-
|
|
9892
|
-
|
|
9893
|
-
|
|
9894
|
-
|
|
9895
|
-
|
|
9896
|
-
|
|
9897
|
-
|
|
9898
|
-
|
|
9899
|
-
|
|
10703
|
+
MSG4.prompt,
|
|
10704
|
+
MSG4.promptCancel,
|
|
10705
|
+
MSG4.commandExecute,
|
|
10706
|
+
MSG4.queueCancel,
|
|
10707
|
+
MSG4.scheduledList,
|
|
10708
|
+
MSG4.scheduledCreate,
|
|
10709
|
+
MSG4.scheduledCancel,
|
|
10710
|
+
MSG4.sessionsList,
|
|
10711
|
+
MSG4.sessionsCreate,
|
|
10712
|
+
MSG4.sessionsNativeList,
|
|
10713
|
+
MSG4.sessionsRemove,
|
|
10714
|
+
MSG4.sessionsArchive,
|
|
10715
|
+
MSG4.sessionsUnarchive,
|
|
10716
|
+
MSG4.sessionsRename,
|
|
10717
|
+
MSG4.sessionModelGet,
|
|
10718
|
+
MSG4.sessionModelSet,
|
|
10719
|
+
MSG4.sessionEffortGet,
|
|
10720
|
+
MSG4.sessionEffortSet,
|
|
10721
|
+
MSG4.terminalCreate
|
|
9900
10722
|
]);
|
|
9901
10723
|
function requireJson(contentType) {
|
|
9902
10724
|
return (contentType ?? "").toLowerCase().includes("application/json");
|
|
@@ -9908,6 +10730,12 @@ function safePreviewUrl(v) {
|
|
|
9908
10730
|
return;
|
|
9909
10731
|
}
|
|
9910
10732
|
var RPC_MAX_BODY_BYTES = 16 * 1024 * 1024;
|
|
10733
|
+
function validateInteractionResponsePayload(payload) {
|
|
10734
|
+
const parsed = parseControlPayload(MSG4.interactionRespond, payload);
|
|
10735
|
+
if (!parsed)
|
|
10736
|
+
return null;
|
|
10737
|
+
return parsed;
|
|
10738
|
+
}
|
|
9911
10739
|
var MAX_PERSISTED_ATTACHMENTS = 5;
|
|
9912
10740
|
var MAX_ATTACHMENT_FIELD_LEN = 256;
|
|
9913
10741
|
function boundField(v) {
|
|
@@ -9915,16 +10743,16 @@ function boundField(v) {
|
|
|
9915
10743
|
}
|
|
9916
10744
|
function rpcSessionLocks(type, payload) {
|
|
9917
10745
|
const value = payload;
|
|
9918
|
-
if (type ===
|
|
10746
|
+
if (type === MSG4.prompt) {
|
|
9919
10747
|
return typeof value.sessionAlias === "string" && value.sessionAlias ? { alias: value.sessionAlias, lifecycle: false, turn: "shared" } : undefined;
|
|
9920
10748
|
}
|
|
9921
|
-
if (type ===
|
|
10749
|
+
if (type === MSG4.commandExecute) {
|
|
9922
10750
|
return typeof value.sessionAlias === "string" && value.sessionAlias ? { alias: value.sessionAlias, lifecycle: false, turn: "exclusive" } : undefined;
|
|
9923
10751
|
}
|
|
9924
|
-
if (type ===
|
|
10752
|
+
if (type === MSG4.sessionsRename) {
|
|
9925
10753
|
return typeof value.alias === "string" && value.alias ? { alias: value.alias, lifecycle: true, turn: "none" } : undefined;
|
|
9926
10754
|
}
|
|
9927
|
-
if (type ===
|
|
10755
|
+
if (type === MSG4.sessionsCreate || type === MSG4.sessionsRemove || type === MSG4.sessionsArchive || type === MSG4.sessionsUnarchive) {
|
|
9928
10756
|
return typeof value.alias === "string" && value.alias ? { alias: value.alias, lifecycle: true, turn: "exclusive" } : undefined;
|
|
9929
10757
|
}
|
|
9930
10758
|
return;
|
|
@@ -10016,6 +10844,20 @@ function createApp(deps) {
|
|
|
10016
10844
|
const now = deps.now ?? (() => new Date);
|
|
10017
10845
|
const acquireSessionLifecycleRpcLock = createKeyedRpcLock();
|
|
10018
10846
|
const sessionTurnRpcLock = createKeyedRwLock();
|
|
10847
|
+
deps.interactions?.onClose((closed) => {
|
|
10848
|
+
const owner = deps.instances.getOwned(closed.instanceId, closed.accountId);
|
|
10849
|
+
if (owner === null)
|
|
10850
|
+
return;
|
|
10851
|
+
deps.gateway.broadcastControlEvent?.(closed.accountId, {
|
|
10852
|
+
type: "interaction-closed",
|
|
10853
|
+
chatKey: `relay:${closed.accountId}`,
|
|
10854
|
+
sessionAlias: "",
|
|
10855
|
+
instanceId: owner.id,
|
|
10856
|
+
requestId: closed.requestId,
|
|
10857
|
+
reason: closed.reason,
|
|
10858
|
+
...closed.action !== undefined && (closed.action === "accept" || closed.action === "decline" || closed.action === "cancel") ? { action: closed.action } : {}
|
|
10859
|
+
});
|
|
10860
|
+
});
|
|
10019
10861
|
const loginFailures = new Map;
|
|
10020
10862
|
let globalFailures = { count: 0, windowStart: 0 };
|
|
10021
10863
|
function sweepPerIpLoginFailures(nowMs) {
|
|
@@ -10286,27 +11128,61 @@ function createApp(deps) {
|
|
|
10286
11128
|
...payload,
|
|
10287
11129
|
chatKey: `relay:${account.id}`,
|
|
10288
11130
|
senderId: account.id,
|
|
10289
|
-
isOwner: true
|
|
11131
|
+
isOwner: true,
|
|
11132
|
+
chatType: "direct"
|
|
10290
11133
|
};
|
|
10291
11134
|
}
|
|
11135
|
+
if (body.type === MSG4.conversationPrompt || body.type === MSG4.conversationPromptWithPolicy) {
|
|
11136
|
+
payload = {
|
|
11137
|
+
...payload,
|
|
11138
|
+
humanIngress: {
|
|
11139
|
+
chatKey: `relay:${account.id}`,
|
|
11140
|
+
senderId: account.id,
|
|
11141
|
+
accountId: account.id,
|
|
11142
|
+
senderName: account.username,
|
|
11143
|
+
isOwner: true,
|
|
11144
|
+
chatType: "direct"
|
|
11145
|
+
}
|
|
11146
|
+
};
|
|
11147
|
+
}
|
|
11148
|
+
if (body.type === MSG4.interactionRespond) {
|
|
11149
|
+
const answer = validateInteractionResponsePayload(payload);
|
|
11150
|
+
if (!answer)
|
|
11151
|
+
return c.json({ error: "invalid-payload" }, 400);
|
|
11152
|
+
const registry = deps.interactions;
|
|
11153
|
+
if (!registry)
|
|
11154
|
+
return c.json({ error: "interaction-unavailable" }, 503);
|
|
11155
|
+
const pending = registry.get(answer.requestId);
|
|
11156
|
+
if (!pending || pending.accountId !== account.id || pending.kind !== answer.kind) {
|
|
11157
|
+
return c.json({ error: "interaction-gone" }, 409);
|
|
11158
|
+
}
|
|
11159
|
+
const closed = registry.answer(answer.requestId, answer);
|
|
11160
|
+
if (!closed) {
|
|
11161
|
+
return c.json({ error: "interaction-gone" }, 409);
|
|
11162
|
+
}
|
|
11163
|
+
return c.json({ ok: true });
|
|
11164
|
+
}
|
|
11165
|
+
if (body.type === MSG4.interactionRequest || body.type === MSG4.interactionWithdraw) {
|
|
11166
|
+
return c.json({ error: "connector-only" }, 403);
|
|
11167
|
+
}
|
|
10292
11168
|
const releaseSessionRpcLocks = [];
|
|
10293
11169
|
let persistedPromptId;
|
|
10294
11170
|
let webPromptRequestId;
|
|
10295
11171
|
try {
|
|
10296
|
-
if (body.type ===
|
|
11172
|
+
if (body.type === MSG4.prompt && !parseControlPayload(MSG4.prompt, payload)) {
|
|
10297
11173
|
return c.json({ error: "invalid-payload" }, 400);
|
|
10298
11174
|
}
|
|
10299
|
-
if (body.type ===
|
|
11175
|
+
if (body.type === MSG4.commandExecute && !parseControlPayload(MSG4.commandExecute, payload)) {
|
|
10300
11176
|
return c.json({ error: "invalid-payload" }, 400);
|
|
10301
11177
|
}
|
|
10302
|
-
if (body.type ===
|
|
11178
|
+
if (body.type === MSG4.upload && !parseControlPayload(MSG4.upload, payload)) {
|
|
10303
11179
|
return c.json({ error: "invalid-payload" }, 400);
|
|
10304
11180
|
}
|
|
10305
|
-
const removeInput = body.type ===
|
|
10306
|
-
if (body.type ===
|
|
11181
|
+
const removeInput = body.type === MSG4.sessionsRemove ? parseControlPayload(MSG4.sessionsRemove, payload) : undefined;
|
|
11182
|
+
if (body.type === MSG4.sessionsRemove && !removeInput) {
|
|
10307
11183
|
return c.json({ error: "invalid-payload" }, 400);
|
|
10308
11184
|
}
|
|
10309
|
-
if (body.type ===
|
|
11185
|
+
if (body.type === MSG4.upload) {
|
|
10310
11186
|
const up = payload;
|
|
10311
11187
|
const approxBytes = up.content ? Math.floor(up.content.length * 3 / 4) : 0;
|
|
10312
11188
|
if (approxBytes > 10 * 1024 * 1024)
|
|
@@ -10324,7 +11200,7 @@ function createApp(deps) {
|
|
|
10324
11200
|
releaseSessionRpcLocks.push(await sessionTurnRpcLock.acquireExclusive(key));
|
|
10325
11201
|
}
|
|
10326
11202
|
}
|
|
10327
|
-
if (body.type ===
|
|
11203
|
+
if (body.type === MSG4.prompt || body.type === MSG4.commandExecute) {
|
|
10328
11204
|
const p = payload;
|
|
10329
11205
|
if (p.sessionAlias && p.text !== undefined) {
|
|
10330
11206
|
const attachments = (p.media ?? []).slice(0, MAX_PERSISTED_ATTACHMENTS).map((m) => {
|
|
@@ -10339,11 +11215,11 @@ function createApp(deps) {
|
|
|
10339
11215
|
};
|
|
10340
11216
|
});
|
|
10341
11217
|
const promptRequestId = randomUUID4();
|
|
10342
|
-
if (body.type ===
|
|
11218
|
+
if (body.type === MSG4.prompt) {
|
|
10343
11219
|
webPromptRequestId = promptRequestId;
|
|
10344
11220
|
}
|
|
10345
11221
|
persistedPromptId = deps.messages.append(instance.id, p.sessionAlias, "in", p.text, undefined, attachments, promptRequestId);
|
|
10346
|
-
if (body.type ===
|
|
11222
|
+
if (body.type === MSG4.prompt && typeof payload === "object" && payload !== null) {
|
|
10347
11223
|
payload.promptRequestId = promptRequestId;
|
|
10348
11224
|
deps.onWebPromptCreated?.({
|
|
10349
11225
|
promptRequestId,
|
|
@@ -10354,11 +11230,11 @@ function createApp(deps) {
|
|
|
10354
11230
|
}
|
|
10355
11231
|
}
|
|
10356
11232
|
const result = await deps.gateway.sendRequest(instance.id, body.type, payload);
|
|
10357
|
-
const isPromptRejection = body.type ===
|
|
11233
|
+
const isPromptRejection = body.type === MSG4.prompt && typeof result === "object" && result !== null && "ok" in result && result.ok === false;
|
|
10358
11234
|
if (webPromptRequestId && (isErrorPayload2(result) || isPromptRejection)) {
|
|
10359
11235
|
deps.onWebPromptRejected?.(webPromptRequestId);
|
|
10360
11236
|
}
|
|
10361
|
-
if (body.type ===
|
|
11237
|
+
if (body.type === MSG4.prompt && persistedPromptId !== undefined && typeof result === "object" && result !== null && result.queued === true && typeof result.queueItemId === "string") {
|
|
10362
11238
|
const p = payload;
|
|
10363
11239
|
const queueItemId = result.queueItemId;
|
|
10364
11240
|
deps.messages.markQueued(persistedPromptId, {
|
|
@@ -10374,7 +11250,7 @@ function createApp(deps) {
|
|
|
10374
11250
|
});
|
|
10375
11251
|
}
|
|
10376
11252
|
}
|
|
10377
|
-
if (body.type ===
|
|
11253
|
+
if (body.type === MSG4.queueCancel && !isErrorPayload2(result)) {
|
|
10378
11254
|
const p = payload;
|
|
10379
11255
|
if (typeof p.itemId === "string") {
|
|
10380
11256
|
const cancelResult = result;
|
|
@@ -10383,7 +11259,7 @@ function createApp(deps) {
|
|
|
10383
11259
|
}
|
|
10384
11260
|
}
|
|
10385
11261
|
}
|
|
10386
|
-
if ((body.type ===
|
|
11262
|
+
if ((body.type === MSG4.sessionsRemove || body.type === MSG4.sessionsArchive) && !isErrorPayload2(result)) {
|
|
10387
11263
|
const targetAlias = removeInput ? removeInput.alias : typeof payload === "object" && payload !== null && ("alias" in payload) && typeof payload.alias === "string" ? payload.alias : undefined;
|
|
10388
11264
|
if (targetAlias) {
|
|
10389
11265
|
deps.onWebPromptSessionCleared?.(instance.id, targetAlias);
|
|
@@ -10393,7 +11269,7 @@ function createApp(deps) {
|
|
|
10393
11269
|
deps.messages.deleteBySession(instance.id, removeInput.alias);
|
|
10394
11270
|
deps.turnSlotAnchors?.deleteBySession(instance.id, removeInput.alias);
|
|
10395
11271
|
}
|
|
10396
|
-
if (body.type ===
|
|
11272
|
+
if (body.type === MSG4.commandExecute) {
|
|
10397
11273
|
const p = payload;
|
|
10398
11274
|
const output = result?.output;
|
|
10399
11275
|
if (p.sessionAlias && typeof output === "string")
|
|
@@ -10533,13 +11409,23 @@ async function createRelayRuntime(dbPath, options = {}) {
|
|
|
10533
11409
|
const instances = new InstanceStore(db);
|
|
10534
11410
|
const messages = new MessageStore(db);
|
|
10535
11411
|
let gatewayRef = null;
|
|
11412
|
+
const desktopStreamOwners = new Map;
|
|
11413
|
+
const desktop = new DesktopStreamGateway({
|
|
11414
|
+
logger,
|
|
11415
|
+
onStreamClosed: (streamId) => {
|
|
11416
|
+
desktopStreamOwners.delete(streamId);
|
|
11417
|
+
}
|
|
11418
|
+
});
|
|
10536
11419
|
const webGateway = new WebGateway({
|
|
10537
11420
|
logger,
|
|
10538
11421
|
onAttachmentDetached: (info) => {
|
|
10539
|
-
gatewayRef?.sendEvent(info.instanceId,
|
|
11422
|
+
gatewayRef?.sendEvent(info.instanceId, MSG5.terminalDetach, {
|
|
10540
11423
|
attachmentId: info.attachmentId,
|
|
10541
11424
|
viewerId: info.viewerId
|
|
10542
11425
|
});
|
|
11426
|
+
},
|
|
11427
|
+
onViewerClosed: (viewerId) => {
|
|
11428
|
+
cancelViewerDesktopStreams(gatewayRef, desktop, desktopStreamOwners, viewerId);
|
|
10543
11429
|
}
|
|
10544
11430
|
});
|
|
10545
11431
|
const pushSubscriptions = new PushSubscriptionStore(db);
|
|
@@ -10667,7 +11553,9 @@ async function createRelayRuntime(dbPath, options = {}) {
|
|
|
10667
11553
|
parts: a.parts,
|
|
10668
11554
|
status: a.text ? "streaming" : "working",
|
|
10669
11555
|
startedAt: a.startedAt,
|
|
10670
|
-
...typeof a.slotAfterId === "number" ? { slotAfterId: a.slotAfterId } : {}
|
|
11556
|
+
...typeof a.slotAfterId === "number" ? { slotAfterId: a.slotAfterId } : {},
|
|
11557
|
+
...a.conversation ? { conversation: a.conversation } : {},
|
|
11558
|
+
...a.truncated ? { truncated: true } : {}
|
|
10671
11559
|
});
|
|
10672
11560
|
}
|
|
10673
11561
|
return out;
|
|
@@ -10702,17 +11590,30 @@ async function createRelayRuntime(dbPath, options = {}) {
|
|
|
10702
11590
|
a.parts.push({ type: "tool", step });
|
|
10703
11591
|
};
|
|
10704
11592
|
const pendingCompletionRoutes = new PendingCompletionRouteStore(db);
|
|
11593
|
+
const interactions = new InteractionRegistry({
|
|
11594
|
+
debug: (event, message, fields) => logger.debug(event, message, fields)
|
|
11595
|
+
});
|
|
11596
|
+
const broadcastControlEvent = (accountId, event) => {
|
|
11597
|
+
webGateway.broadcast(accountId, {
|
|
11598
|
+
kind: "control-event",
|
|
11599
|
+
instanceId: event.instanceId ?? "",
|
|
11600
|
+
event
|
|
11601
|
+
});
|
|
11602
|
+
};
|
|
10705
11603
|
const gateway = new InstanceGateway({
|
|
10706
11604
|
instances,
|
|
10707
11605
|
accounts,
|
|
10708
11606
|
requestTimeoutMs: options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
|
|
10709
11607
|
logger,
|
|
10710
11608
|
pendingCompletionRoutes,
|
|
11609
|
+
interactions,
|
|
11610
|
+
broadcastControlEvent,
|
|
10711
11611
|
onDirectoryChange: (accountId, endpoints) => {
|
|
10712
11612
|
webGateway.broadcast(accountId, { kind: "agent-directory", endpoints });
|
|
10713
11613
|
},
|
|
10714
11614
|
onStatusChange: (instanceId, accountId, online) => {
|
|
10715
11615
|
if (!online) {
|
|
11616
|
+
desktop.closeForInstance(instanceId, "instance-offline");
|
|
10716
11617
|
const prefix = `${instanceId}\x00`;
|
|
10717
11618
|
for (const k of turnBuffers.keys())
|
|
10718
11619
|
if (k.startsWith(prefix))
|
|
@@ -10822,7 +11723,7 @@ async function createRelayRuntime(dbPath, options = {}) {
|
|
|
10822
11723
|
messages.append(instanceId, sessionAlias, "out", text, structured, undefined, undefined, undefined, slot.startedAt, slot.slotAfterId, slot.startedAfterSeq);
|
|
10823
11724
|
};
|
|
10824
11725
|
try {
|
|
10825
|
-
if (envelope.type ===
|
|
11726
|
+
if (envelope.type === MSG5.instanceEvent) {
|
|
10826
11727
|
const raw = envelope.payload?.event;
|
|
10827
11728
|
if (!validControlEvent(raw)) {
|
|
10828
11729
|
logger.debug("relay.event.invalid", "dropped malformed instance event", {
|
|
@@ -10870,7 +11771,8 @@ async function createRelayRuntime(dbPath, options = {}) {
|
|
|
10870
11771
|
startedAt,
|
|
10871
11772
|
slotAfterId: slot.slotAfterId,
|
|
10872
11773
|
...slot.startedAfterSeq !== undefined ? { startedAfterSeq: slot.startedAfterSeq } : {},
|
|
10873
|
-
...notification ? { notification } : {}
|
|
11774
|
+
...notification ? { notification } : {},
|
|
11775
|
+
...event.conversation ? { conversation: event.conversation } : {}
|
|
10874
11776
|
});
|
|
10875
11777
|
outbound = { ...event, slotAfterId: slot.slotAfterId, startedAt };
|
|
10876
11778
|
} catch (err) {
|
|
@@ -10961,7 +11863,7 @@ async function createRelayRuntime(dbPath, options = {}) {
|
|
|
10961
11863
|
if (promptRequestId) {
|
|
10962
11864
|
removePendingWebPrompt(promptRequestId);
|
|
10963
11865
|
}
|
|
10964
|
-
gateway.sendEvent(instanceId,
|
|
11866
|
+
gateway.sendEvent(instanceId, MSG5.instanceRecoveryAck, { recoveryIds: [recoveryId] });
|
|
10965
11867
|
} else {
|
|
10966
11868
|
flush();
|
|
10967
11869
|
if (promptRequestId) {
|
|
@@ -10991,9 +11893,13 @@ async function createRelayRuntime(dbPath, options = {}) {
|
|
|
10991
11893
|
});
|
|
10992
11894
|
}
|
|
10993
11895
|
} else if (event.type === "turn-usage") {
|
|
10994
|
-
|
|
11896
|
+
if (!event.conversation) {
|
|
11897
|
+
sessionUsage.set(key(instanceId, event.sessionAlias), { used: event.used, size: event.size, ...event.cost ? { cost: event.cost } : {}, ...event.breakdown ? { breakdown: event.breakdown } : {} });
|
|
11898
|
+
}
|
|
10995
11899
|
} else if (event.type === "agent-commands") {
|
|
10996
|
-
|
|
11900
|
+
if (!event.conversation) {
|
|
11901
|
+
sessionCommands.set(key(instanceId, event.sessionAlias), event.commands);
|
|
11902
|
+
}
|
|
10997
11903
|
} else if (event.type === "session-history") {
|
|
10998
11904
|
const existing = messages.listBySession(accountId, instanceId, event.sessionAlias, { limit: 1 });
|
|
10999
11905
|
if (existing.messages.length === 0) {
|
|
@@ -11010,7 +11916,7 @@ async function createRelayRuntime(dbPath, options = {}) {
|
|
|
11010
11916
|
} else if (event.type === "agent-message-completion") {
|
|
11011
11917
|
messages.patchAgentMessageCompletionStatus(instanceId, event.sessionAlias, event.messageId, event.completionStatus);
|
|
11012
11918
|
}
|
|
11013
|
-
} else if (envelope.type ===
|
|
11919
|
+
} else if (envelope.type === MSG5.instanceStateSync) {
|
|
11014
11920
|
if (!validInstanceStateSync(envelope.payload)) {
|
|
11015
11921
|
logger.debug("relay.event.invalid", "dropped malformed instance state sync", { instanceId });
|
|
11016
11922
|
return;
|
|
@@ -11117,7 +12023,7 @@ async function createRelayRuntime(dbPath, options = {}) {
|
|
|
11117
12023
|
}
|
|
11118
12024
|
}
|
|
11119
12025
|
if (ackedRecoveryIds.length > 0) {
|
|
11120
|
-
gateway.sendEvent(instanceId,
|
|
12026
|
+
gateway.sendEvent(instanceId, MSG5.instanceRecoveryAck, { recoveryIds: ackedRecoveryIds });
|
|
11121
12027
|
}
|
|
11122
12028
|
for (const turn of sync.turns) {
|
|
11123
12029
|
reconcileInboundPrompt(turn.sessionAlias, { prompt: turn.prompt, queueItemId: turn.queueItemId, scheduled: turn.scheduled, promptRequestId: turn.promptRequestId }, false);
|
|
@@ -11170,13 +12076,27 @@ async function createRelayRuntime(dbPath, options = {}) {
|
|
|
11170
12076
|
};
|
|
11171
12077
|
})(),
|
|
11172
12078
|
...turn.truncated ? { truncated: true } : {},
|
|
11173
|
-
...notification ? { notification } : {}
|
|
12079
|
+
...notification ? { notification } : {},
|
|
12080
|
+
...turn.conversation ? { conversation: turn.conversation } : {}
|
|
11174
12081
|
});
|
|
11175
12082
|
}
|
|
12083
|
+
const productSessionAliases = new Set;
|
|
12084
|
+
for (const turn of sync.turns) {
|
|
12085
|
+
if (turn.conversation)
|
|
12086
|
+
productSessionAliases.add(turn.sessionAlias);
|
|
12087
|
+
}
|
|
12088
|
+
for (const finished of sync.finishedOffline) {
|
|
12089
|
+
if (finished.conversation)
|
|
12090
|
+
productSessionAliases.add(finished.sessionAlias);
|
|
12091
|
+
}
|
|
11176
12092
|
for (const meter of sync.usage) {
|
|
12093
|
+
if (productSessionAliases.has(meter.sessionAlias))
|
|
12094
|
+
continue;
|
|
11177
12095
|
sessionUsage.set(key(instanceId, meter.sessionAlias), { used: meter.used, size: meter.size, ...meter.cost ? { cost: meter.cost } : {}, ...meter.breakdown ? { breakdown: meter.breakdown } : {} });
|
|
11178
12096
|
}
|
|
11179
12097
|
for (const entry of sync.commands) {
|
|
12098
|
+
if (productSessionAliases.has(entry.sessionAlias))
|
|
12099
|
+
continue;
|
|
11180
12100
|
sessionCommands.set(key(instanceId, entry.sessionAlias), entry.commands);
|
|
11181
12101
|
}
|
|
11182
12102
|
const keepAnchors = new Set;
|
|
@@ -11190,7 +12110,7 @@ async function createRelayRuntime(dbPath, options = {}) {
|
|
|
11190
12110
|
gateway.disconnect(instanceId);
|
|
11191
12111
|
throw err;
|
|
11192
12112
|
}
|
|
11193
|
-
} else if (envelope.type ===
|
|
12113
|
+
} else if (envelope.type === MSG5.instanceNotice) {
|
|
11194
12114
|
const notice = envelope.payload;
|
|
11195
12115
|
webGateway.broadcast(accountId, { kind: "notice", instanceId, notice });
|
|
11196
12116
|
if (notice.kind === "task-completion") {
|
|
@@ -11220,6 +12140,7 @@ async function createRelayRuntime(dbPath, options = {}) {
|
|
|
11220
12140
|
checkUpdate: createRelayUpdateChecker({ current: readRelayVersion() }),
|
|
11221
12141
|
vapidPublicKey: vapid ? () => vapid.publicKey : undefined,
|
|
11222
12142
|
pushSubscriptions,
|
|
12143
|
+
interactions,
|
|
11223
12144
|
onWebPromptCreated: ({ promptRequestId, instanceId, sessionAlias }) => {
|
|
11224
12145
|
recordPendingWebPrompt(promptRequestId, instanceId, sessionAlias);
|
|
11225
12146
|
},
|
|
@@ -11239,6 +12160,8 @@ async function createRelayRuntime(dbPath, options = {}) {
|
|
|
11239
12160
|
});
|
|
11240
12161
|
const completionRouteSweepTimer = setInterval(() => gateway.sweepExpiredCompletionRoutes(), 60 * 60000);
|
|
11241
12162
|
completionRouteSweepTimer.unref?.();
|
|
12163
|
+
const desktopSweepTimer = setInterval(() => desktop.sweepExpired(), DESKTOP_TICKET_TTL_MS2);
|
|
12164
|
+
desktopSweepTimer.unref?.();
|
|
11242
12165
|
return {
|
|
11243
12166
|
db,
|
|
11244
12167
|
accounts,
|
|
@@ -11250,11 +12173,15 @@ async function createRelayRuntime(dbPath, options = {}) {
|
|
|
11250
12173
|
pushNotifier,
|
|
11251
12174
|
gateway,
|
|
11252
12175
|
webGateway,
|
|
12176
|
+
desktop,
|
|
12177
|
+
desktopStreamOwners,
|
|
11253
12178
|
stateSnapshot,
|
|
12179
|
+
interactions,
|
|
11254
12180
|
pendingWebPromptsCount: () => pendingWebPrompts.size,
|
|
11255
12181
|
app,
|
|
11256
12182
|
close: () => {
|
|
11257
12183
|
clearInterval(completionRouteSweepTimer);
|
|
12184
|
+
clearInterval(desktopSweepTimer);
|
|
11258
12185
|
db.close();
|
|
11259
12186
|
}
|
|
11260
12187
|
};
|
|
@@ -11280,16 +12207,61 @@ async function startRelayServer(options) {
|
|
|
11280
12207
|
}
|
|
11281
12208
|
});
|
|
11282
12209
|
const dedicated = options.wsPort !== undefined;
|
|
12210
|
+
const webWss = new WebSocketServer({ noServer: true, maxPayload: WEB_CLIENT_MAX_PAYLOAD_BYTES });
|
|
12211
|
+
const desktopBrowserWss = new WebSocketServer({ noServer: true, maxPayload: DESKTOP_WS_MAX_PAYLOAD_BYTES2 });
|
|
12212
|
+
const desktopConnectorWss = new WebSocketServer({ noServer: true, maxPayload: DESKTOP_WS_MAX_PAYLOAD_BYTES2 });
|
|
11283
12213
|
let wss;
|
|
11284
12214
|
let gatewayWss;
|
|
12215
|
+
let dedicatedControlWss;
|
|
12216
|
+
let gatewayHttpServer;
|
|
11285
12217
|
if (dedicated) {
|
|
11286
|
-
|
|
11287
|
-
|
|
11288
|
-
|
|
12218
|
+
const dedicatedHttpServer = createServer((req, res) => {
|
|
12219
|
+
res.writeHead(426, { "Content-Type": "text/plain" });
|
|
12220
|
+
res.end("Upgrade Required");
|
|
12221
|
+
});
|
|
12222
|
+
gatewayHttpServer = dedicatedHttpServer;
|
|
12223
|
+
wss = new WebSocketServer({ noServer: true, maxPayload: DESKTOP_WS_MAX_PAYLOAD_BYTES2 });
|
|
12224
|
+
dedicatedControlWss = new WebSocketServer({ noServer: true });
|
|
12225
|
+
dedicatedHttpServer.on("upgrade", (req, socket, head) => {
|
|
12226
|
+
const path = (req.url ?? "").split("?")[0] ?? "";
|
|
12227
|
+
if (path === "/desktop/instance") {
|
|
12228
|
+
const ticket = desktopTicketFromUrl(req.url ?? "");
|
|
12229
|
+
if (!ticket) {
|
|
12230
|
+
try {
|
|
12231
|
+
socket.destroy();
|
|
12232
|
+
} catch {}
|
|
12233
|
+
return;
|
|
12234
|
+
}
|
|
12235
|
+
const precheck = runtime.desktop.precheckConnectorTicket(ticket);
|
|
12236
|
+
if (!precheck.ok) {
|
|
12237
|
+
try {
|
|
12238
|
+
socket.destroy();
|
|
12239
|
+
} catch {}
|
|
12240
|
+
return;
|
|
12241
|
+
}
|
|
12242
|
+
desktopConnectorWss.handleUpgrade(req, socket, head, (ws) => {
|
|
12243
|
+
const attached = runtime.desktop.attachConnector(precheck.claim, adaptDesktopSocket(ws));
|
|
12244
|
+
if (!attached.ok) {
|
|
12245
|
+
try {
|
|
12246
|
+
ws.close(4403, attached.reason);
|
|
12247
|
+
} catch {}
|
|
12248
|
+
}
|
|
12249
|
+
});
|
|
12250
|
+
return;
|
|
12251
|
+
}
|
|
12252
|
+
dedicatedControlWss.handleUpgrade(req, socket, head, (ws) => runtime.gateway.handleConnection(ws));
|
|
12253
|
+
});
|
|
12254
|
+
await new Promise((resolve, reject) => {
|
|
12255
|
+
const onErr = (err) => reject(err instanceof Error ? err : new Error(String(err)));
|
|
12256
|
+
dedicatedHttpServer.once("error", onErr);
|
|
12257
|
+
dedicatedHttpServer.listen(options.wsPort, host, () => {
|
|
12258
|
+
dedicatedHttpServer.removeListener("error", onErr);
|
|
12259
|
+
resolve();
|
|
12260
|
+
});
|
|
12261
|
+
});
|
|
11289
12262
|
} else {
|
|
11290
12263
|
gatewayWss = new WebSocketServer({ noServer: true });
|
|
11291
12264
|
}
|
|
11292
|
-
const webWss = new WebSocketServer({ noServer: true, maxPayload: WEB_CLIENT_MAX_PAYLOAD_BYTES });
|
|
11293
12265
|
httpServer.on("upgrade", (req, socket, head) => {
|
|
11294
12266
|
const path = (req.url ?? "").split("?")[0] ?? "";
|
|
11295
12267
|
if (path === "/ws") {
|
|
@@ -11305,19 +12277,88 @@ async function startRelayServer(options) {
|
|
|
11305
12277
|
instances: runtime.instances,
|
|
11306
12278
|
gateway: runtime.gateway,
|
|
11307
12279
|
webGateway: runtime.webGateway,
|
|
11308
|
-
stateSnapshot: runtime.stateSnapshot
|
|
12280
|
+
stateSnapshot: runtime.stateSnapshot,
|
|
12281
|
+
desktop: {
|
|
12282
|
+
reserve: (reserveAccountId, instanceId) => {
|
|
12283
|
+
const reserved = runtime.desktop.reserve({
|
|
12284
|
+
accountId: reserveAccountId,
|
|
12285
|
+
instanceId,
|
|
12286
|
+
ttlMs: DESKTOP_TICKET_TTL_MS2
|
|
12287
|
+
});
|
|
12288
|
+
if (!reserved.ok)
|
|
12289
|
+
return reserved;
|
|
12290
|
+
return { ok: true, streamId: reserved.record.streamId };
|
|
12291
|
+
},
|
|
12292
|
+
mintConnectorTicket: (streamId, ticketAccountId, instanceId) => runtime.desktop.ticketStore.mintTicket({ streamId, accountId: ticketAccountId, instanceId, side: "connector" }),
|
|
12293
|
+
mintBrowserTicket: (streamId, ticketAccountId, instanceId) => runtime.desktop.mintBrowserTicket({ streamId, accountId: ticketAccountId, instanceId }),
|
|
12294
|
+
markReady: (streamId, security) => runtime.desktop.reportConnectorReady(streamId, security),
|
|
12295
|
+
cancel: (streamId, reason) => {
|
|
12296
|
+
runtime.desktopStreamOwners.delete(streamId);
|
|
12297
|
+
runtime.desktop.closeStream(streamId, reason);
|
|
12298
|
+
},
|
|
12299
|
+
cancelPendingByRequest: (requestId, ownerViewerId, reason) => cancelViewerDesktopStreamByRequest(runtime.gateway, runtime.desktop, runtime.desktopStreamOwners, requestId, ownerViewerId, reason),
|
|
12300
|
+
ownsStream: (streamId, ownerViewerId) => runtime.desktopStreamOwners.get(streamId)?.viewerId === ownerViewerId,
|
|
12301
|
+
streamOwner: (streamId) => runtime.desktopStreamOwners.get(streamId),
|
|
12302
|
+
trackOwner: (streamId, owner) => {
|
|
12303
|
+
runtime.desktopStreamOwners.set(streamId, owner);
|
|
12304
|
+
}
|
|
12305
|
+
},
|
|
12306
|
+
interactions: runtime.interactions
|
|
11309
12307
|
}, account.id, ws, String(data)));
|
|
11310
12308
|
});
|
|
11311
12309
|
return;
|
|
11312
12310
|
}
|
|
12311
|
+
if (path === "/desktop/observe") {
|
|
12312
|
+
const token = parseCookie(req.headers.cookie ?? "")["xrelay_session"];
|
|
12313
|
+
const account = token ? runtime.accounts.getSessionAccount(token) : null;
|
|
12314
|
+
if (!account) {
|
|
12315
|
+
socket.destroy();
|
|
12316
|
+
return;
|
|
12317
|
+
}
|
|
12318
|
+
const ticket = desktopTicketFromUrl(req.url ?? "");
|
|
12319
|
+
if (!ticket) {
|
|
12320
|
+
socket.destroy();
|
|
12321
|
+
return;
|
|
12322
|
+
}
|
|
12323
|
+
desktopBrowserWss.handleUpgrade(req, socket, head, (ws) => {
|
|
12324
|
+
const attached = runtime.desktop.attachBrowser(ticket, adaptDesktopSocket(ws), account.id);
|
|
12325
|
+
if (!attached.ok) {
|
|
12326
|
+
try {
|
|
12327
|
+
ws.close(4403, attached.reason);
|
|
12328
|
+
} catch {}
|
|
12329
|
+
}
|
|
12330
|
+
});
|
|
12331
|
+
return;
|
|
12332
|
+
}
|
|
11313
12333
|
if (gatewayWss && (path === "/" || path === "/gateway" || path.startsWith("/gateway/"))) {
|
|
11314
12334
|
gatewayWss.handleUpgrade(req, socket, head, (ws) => runtime.gateway.handleConnection(ws));
|
|
11315
12335
|
return;
|
|
11316
12336
|
}
|
|
12337
|
+
if (!dedicated && path === "/desktop/instance") {
|
|
12338
|
+
const ticket = desktopTicketFromUrl(req.url ?? "");
|
|
12339
|
+
if (!ticket) {
|
|
12340
|
+
socket.destroy();
|
|
12341
|
+
return;
|
|
12342
|
+
}
|
|
12343
|
+
const precheck = runtime.desktop.precheckConnectorTicket(ticket);
|
|
12344
|
+
if (!precheck.ok) {
|
|
12345
|
+
socket.destroy();
|
|
12346
|
+
return;
|
|
12347
|
+
}
|
|
12348
|
+
desktopConnectorWss.handleUpgrade(req, socket, head, (ws) => {
|
|
12349
|
+
const attached = runtime.desktop.attachConnector(precheck.claim, adaptDesktopSocket(ws));
|
|
12350
|
+
if (!attached.ok) {
|
|
12351
|
+
try {
|
|
12352
|
+
ws.close(4403, attached.reason);
|
|
12353
|
+
} catch {}
|
|
12354
|
+
}
|
|
12355
|
+
});
|
|
12356
|
+
return;
|
|
12357
|
+
}
|
|
11317
12358
|
socket.destroy();
|
|
11318
12359
|
});
|
|
11319
12360
|
const httpPort = httpServer.address().port;
|
|
11320
|
-
const wsPort =
|
|
12361
|
+
const wsPort = dedicated && gatewayHttpServer ? gatewayHttpServer.address()?.port ?? null : null;
|
|
11321
12362
|
return {
|
|
11322
12363
|
runtime,
|
|
11323
12364
|
httpPort,
|
|
@@ -11325,15 +12366,46 @@ async function startRelayServer(options) {
|
|
|
11325
12366
|
close: async () => {
|
|
11326
12367
|
stopMaintenance();
|
|
11327
12368
|
await new Promise((resolve) => webWss.close(() => resolve()));
|
|
12369
|
+
await new Promise((resolve) => desktopBrowserWss.close(() => resolve()));
|
|
12370
|
+
await new Promise((resolve) => desktopConnectorWss.close(() => resolve()));
|
|
11328
12371
|
if (gatewayWss)
|
|
11329
12372
|
await new Promise((resolve) => gatewayWss.close(() => resolve()));
|
|
12373
|
+
if (dedicatedControlWss)
|
|
12374
|
+
await new Promise((resolve) => dedicatedControlWss.close(() => resolve()));
|
|
11330
12375
|
if (wss)
|
|
11331
12376
|
await new Promise((resolve) => wss.close(() => resolve()));
|
|
12377
|
+
if (gatewayHttpServer) {
|
|
12378
|
+
await new Promise((resolve) => gatewayHttpServer.close(() => resolve()));
|
|
12379
|
+
}
|
|
11332
12380
|
await new Promise((resolve) => httpServer.close(() => resolve()));
|
|
11333
12381
|
runtime.close();
|
|
11334
12382
|
}
|
|
11335
12383
|
};
|
|
11336
12384
|
}
|
|
12385
|
+
function desktopTicketFromUrl(url) {
|
|
12386
|
+
const query = url.split("?")[1] ?? "";
|
|
12387
|
+
for (const part of query.split("&")) {
|
|
12388
|
+
const idx = part.indexOf("=");
|
|
12389
|
+
if (idx === -1)
|
|
12390
|
+
continue;
|
|
12391
|
+
if (part.slice(0, idx) !== "ticket")
|
|
12392
|
+
continue;
|
|
12393
|
+
let ticket;
|
|
12394
|
+
try {
|
|
12395
|
+
ticket = decodeURIComponent(part.slice(idx + 1));
|
|
12396
|
+
} catch {
|
|
12397
|
+
return null;
|
|
12398
|
+
}
|
|
12399
|
+
if (ticket.length === 0 || ticket.length > MAX_DESKTOP_TICKET_LENGTH2)
|
|
12400
|
+
return null;
|
|
12401
|
+
return ticket;
|
|
12402
|
+
}
|
|
12403
|
+
return null;
|
|
12404
|
+
}
|
|
12405
|
+
function adaptDesktopSocket(ws) {
|
|
12406
|
+
ws.on("error", () => {});
|
|
12407
|
+
return ws;
|
|
12408
|
+
}
|
|
11337
12409
|
function parseCookie(header) {
|
|
11338
12410
|
const out = {};
|
|
11339
12411
|
for (const part of header.split(";")) {
|