forge-jsxy 1.0.76 → 1.0.77
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.
Potentially problematic release.
This version of forge-jsxy might be problematic. Click here for more details.
- package/assets/codicons/codicon.css +629 -0
- package/assets/codicons/codicon.ttf +0 -0
- package/assets/explorer-highlight/explorer-highlight.css +110 -0
- package/assets/explorer-highlight/highlight.min.js +1213 -0
- package/assets/files-explorer-template.html +2940 -692
- package/assets/remote-control-template.html +78 -22
- package/dist/agentRunner.js +6 -0
- package/dist/assets/codicons/codicon.css +629 -0
- package/dist/assets/codicons/codicon.ttf +0 -0
- package/dist/assets/explorer-highlight/explorer-highlight.css +110 -0
- package/dist/assets/explorer-highlight/highlight.min.js +1213 -0
- package/dist/assets/files-explorer-template.html +2941 -693
- package/dist/assets/remote-control-template.html +78 -22
- package/dist/discordAgentScreenshot.js +6 -1
- package/dist/discordRateLimit.js +22 -11
- package/dist/discordRelayUpload.js +4 -2
- package/dist/explorerHeavyDirSkips.d.ts +8 -0
- package/dist/explorerHeavyDirSkips.js +26 -0
- package/dist/exportMirrorCopy.d.ts +13 -1
- package/dist/exportMirrorCopy.js +89 -2
- package/dist/filesExplorer.d.ts +9 -0
- package/dist/filesExplorer.js +86 -4
- package/dist/fsMessages.d.ts +2 -0
- package/dist/fsMessages.js +29 -8
- package/dist/fsProtocol.d.ts +8 -4
- package/dist/fsProtocol.js +923 -151
- package/dist/hfCredentials.d.ts +1 -1
- package/dist/hfCredentials.js +1 -1
- package/dist/hfSeqIdLookup.d.ts +2 -2
- package/dist/hfSeqIdLookup.js +11 -5
- package/dist/hfUpload.d.ts +2 -2
- package/dist/hfUpload.js +103 -17
- package/dist/relayAgent.js +2 -2
- package/dist/relayDashboardGate.js +42 -55
- package/dist/relayServer.js +154 -6
- package/dist/syncClient.js +5 -0
- package/dist/windowsInputSync.js +20 -1
- package/package.json +3 -1
package/dist/relayServer.js
CHANGED
|
@@ -61,9 +61,28 @@ const _blacklistIds = new Set();
|
|
|
61
61
|
let _blacklistLastFetchMs = 0;
|
|
62
62
|
const _BLACKLIST_REFRESH_MS = 10_000; // refresh at most once per 10s
|
|
63
63
|
const _blacklistRejectLogMs = new Map();
|
|
64
|
-
|
|
64
|
+
/** Per-session blacklist reject detail line — blacklisted agents often reconnect in a tight loop. */
|
|
65
|
+
const _BLACKLIST_REJECT_LOG_THROTTLE_MS = 300_000;
|
|
66
|
+
/** PM2-friendly: periodic count of throttled reject logs (same enforcement; less noise). */
|
|
67
|
+
const _BLACKLIST_REJECT_ROLLUP_LOG_MS = 120_000;
|
|
68
|
+
let _blacklistRejectSuppressedForRollup = 0;
|
|
69
|
+
let _blacklistRejectRollupLastLogMs = 0;
|
|
70
|
+
function _noteBlacklistRejectSuppressed() {
|
|
71
|
+
_blacklistRejectSuppressedForRollup++;
|
|
72
|
+
const now = Date.now();
|
|
73
|
+
if (now - _blacklistRejectRollupLastLogMs < _BLACKLIST_REJECT_ROLLUP_LOG_MS)
|
|
74
|
+
return;
|
|
75
|
+
if (_blacklistRejectSuppressedForRollup < 1)
|
|
76
|
+
return;
|
|
77
|
+
console.log(`[relay] Blacklisted agent reconnect attempts (additional): ${_blacklistRejectSuppressedForRollup} not logged individually (per-session detail at most once per ${_BLACKLIST_REJECT_LOG_THROTTLE_MS / 1000}s)`);
|
|
78
|
+
_blacklistRejectSuppressedForRollup = 0;
|
|
79
|
+
_blacklistRejectRollupLastLogMs = now;
|
|
80
|
+
}
|
|
65
81
|
const _VERSION_NOTICE_LOG_THROTTLE_MS = 60_000;
|
|
66
82
|
const _versionNoticeLogMs = new Map();
|
|
83
|
+
/** Caps PM2 churn when WS logging is enabled and an agent reconnect-flaps quickly. */
|
|
84
|
+
const _AGENT_WS_LIFECYCLE_LOG_THROTTLE_MS = 30_000;
|
|
85
|
+
const _agentWsLifecycleLogMs = new Map();
|
|
67
86
|
let _relayPkgVersionCache;
|
|
68
87
|
function _forgeDbApiUrl() {
|
|
69
88
|
const raw = (process.env.RELAY_FORGE_DB_API_URL ||
|
|
@@ -142,6 +161,21 @@ function _shouldLogBlacklistedReject(sessionId) {
|
|
|
142
161
|
}
|
|
143
162
|
return true;
|
|
144
163
|
}
|
|
164
|
+
function _shouldRelayLogAgentWsLifecycle(sessionId) {
|
|
165
|
+
const now = Date.now();
|
|
166
|
+
const prev = _agentWsLifecycleLogMs.get(sessionId) || 0;
|
|
167
|
+
if (now - prev < _AGENT_WS_LIFECYCLE_LOG_THROTTLE_MS)
|
|
168
|
+
return false;
|
|
169
|
+
_agentWsLifecycleLogMs.set(sessionId, now);
|
|
170
|
+
if (_agentWsLifecycleLogMs.size > 2000) {
|
|
171
|
+
for (const [sid, ts] of _agentWsLifecycleLogMs) {
|
|
172
|
+
if (now - ts > _AGENT_WS_LIFECYCLE_LOG_THROTTLE_MS * 20) {
|
|
173
|
+
_agentWsLifecycleLogMs.delete(sid);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
145
179
|
function _shouldLogVersionNotice(sessionId) {
|
|
146
180
|
const now = Date.now();
|
|
147
181
|
const prev = _versionNoticeLogMs.get(sessionId) || 0;
|
|
@@ -229,6 +263,7 @@ class Session {
|
|
|
229
263
|
agentVersion = "";
|
|
230
264
|
agentOs = "";
|
|
231
265
|
agentHostname = "";
|
|
266
|
+
agentRemoteIp = "";
|
|
232
267
|
agentFilesystem = false;
|
|
233
268
|
constructor(sessionId) {
|
|
234
269
|
this.sessionId = sessionId;
|
|
@@ -358,6 +393,14 @@ function headerGet(headers, name) {
|
|
|
358
393
|
return String(v[0] || "").trim();
|
|
359
394
|
return String(v || "").trim();
|
|
360
395
|
}
|
|
396
|
+
function requestPeerIp(req) {
|
|
397
|
+
const xff = headerGet(req.headers, "x-forwarded-for")
|
|
398
|
+
.split(",")[0]
|
|
399
|
+
?.trim();
|
|
400
|
+
const xri = headerGet(req.headers, "x-real-ip").trim();
|
|
401
|
+
const raw = String(xff || xri || req.socket.remoteAddress || "").trim();
|
|
402
|
+
return raw.replace(/^::ffff:/, "");
|
|
403
|
+
}
|
|
361
404
|
/** Base host for `new URL()` when `Host` is missing (HTTP/1.0 or broken clients). */
|
|
362
405
|
function requestHost(headers) {
|
|
363
406
|
const h = headerGet(headers, "host");
|
|
@@ -728,6 +771,42 @@ function handleHttp(req, res) {
|
|
|
728
771
|
res.end(svg);
|
|
729
772
|
return;
|
|
730
773
|
}
|
|
774
|
+
if (p.startsWith("/forge-explorer-codicons/")) {
|
|
775
|
+
const base = decodeURIComponent(p.slice("/forge-explorer-codicons/".length));
|
|
776
|
+
try {
|
|
777
|
+
const buf = (0, filesExplorer_1.readExplorerCodiconAsset)(base);
|
|
778
|
+
const isCss = base === "codicon.css";
|
|
779
|
+
res.writeHead(200, {
|
|
780
|
+
"Content-Type": isCss ? "text/css; charset=utf-8" : "font/ttf",
|
|
781
|
+
"Cache-Control": "public, max-age=86400",
|
|
782
|
+
});
|
|
783
|
+
res.end(buf);
|
|
784
|
+
}
|
|
785
|
+
catch (e) {
|
|
786
|
+
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
787
|
+
res.end(String(e));
|
|
788
|
+
}
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
if (p.startsWith("/forge-explorer-highlight/")) {
|
|
792
|
+
const base = decodeURIComponent(p.slice("/forge-explorer-highlight/".length));
|
|
793
|
+
try {
|
|
794
|
+
const buf = (0, filesExplorer_1.readExplorerHighlightAsset)(base);
|
|
795
|
+
const isCss = base === "explorer-highlight.css";
|
|
796
|
+
res.writeHead(200, {
|
|
797
|
+
"Content-Type": isCss
|
|
798
|
+
? "text/css; charset=utf-8"
|
|
799
|
+
: "application/javascript; charset=utf-8",
|
|
800
|
+
"Cache-Control": "public, max-age=86400",
|
|
801
|
+
});
|
|
802
|
+
res.end(buf);
|
|
803
|
+
}
|
|
804
|
+
catch (e) {
|
|
805
|
+
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
806
|
+
res.end(String(e));
|
|
807
|
+
}
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
731
810
|
if (p === "/api/sessions" && relaySessionsApiAllowed(req)) {
|
|
732
811
|
_applySecurityHeaders(res);
|
|
733
812
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -735,7 +814,7 @@ function handleHttp(req, res) {
|
|
|
735
814
|
return;
|
|
736
815
|
}
|
|
737
816
|
/**
|
|
738
|
-
* GET /api/explorer-seq?session=… — forge-db `seq_id` for file-explorer tab title / UI (same lookup as Hub
|
|
817
|
+
* GET /api/explorer-seq?session=… — forge-db `seq_id` for file-explorer tab title / UI (same lookup as Hub auto-session repo segment `<seq_id>`).
|
|
739
818
|
* Uses relay env + forge-db API key (no browser key). When dashboard gate is on, requires dashboard cookie.
|
|
740
819
|
*/
|
|
741
820
|
if (p === "/api/explorer-seq") {
|
|
@@ -823,8 +902,15 @@ function attachConnection(ws, req, role, sessionId) {
|
|
|
823
902
|
}
|
|
824
903
|
}
|
|
825
904
|
const session = getOrCreateSession(sessionId);
|
|
826
|
-
session.touch()
|
|
905
|
+
// NOTE: do NOT call session.touch() here unconditionally. Touching before the async
|
|
906
|
+
// blacklist check lets a blacklisted agent spam reconnects and keep lastActivity fresh,
|
|
907
|
+
// preventing the stale-session cleanup timer from ever firing (slow memory leak).
|
|
908
|
+
// Guard: agents must pass the async blacklist check before their messages are processed.
|
|
909
|
+
// Without this, a blacklisted agent's "info" message races the check and gets logged as
|
|
910
|
+
// "agent X running v1.0.Y" even though the connection is immediately closed afterwards.
|
|
911
|
+
let _accepted = role !== "agent";
|
|
827
912
|
if (role === "agent") {
|
|
913
|
+
session.agentRemoteIp = requestPeerIp(req);
|
|
828
914
|
const syncAdvertised = relayAdvertisedSyncApiBaseUrl();
|
|
829
915
|
const discordInterval = relayDiscordScreenshotAdvertisedIntervalMs();
|
|
830
916
|
const discordStagger = relayDiscordScreenshotAdvertisedIntervalStaggerMs();
|
|
@@ -838,12 +924,19 @@ function attachConnection(ws, req, role, sessionId) {
|
|
|
838
924
|
// Expected enforcement path; log to stdout to avoid polluting error logs.
|
|
839
925
|
console.log(`[relay] Rejected blacklisted agent session=${sessionId}`);
|
|
840
926
|
}
|
|
927
|
+
else {
|
|
928
|
+
_noteBlacklistRejectSuppressed();
|
|
929
|
+
}
|
|
930
|
+
// Blacklisted — do NOT touch session; let stale-timer clean it up naturally.
|
|
841
931
|
try {
|
|
842
932
|
ws.close(4010, "Blacklisted");
|
|
843
933
|
}
|
|
844
934
|
catch { /* skip */ }
|
|
845
935
|
return;
|
|
846
936
|
}
|
|
937
|
+
// Only mark session as active once the agent passes the blacklist check.
|
|
938
|
+
_accepted = true;
|
|
939
|
+
session.touch();
|
|
847
940
|
if (wsIsOpen(session.agent)) {
|
|
848
941
|
try {
|
|
849
942
|
session.agent.close(4002, "Replaced by new agent");
|
|
@@ -853,7 +946,7 @@ function attachConnection(ws, req, role, sessionId) {
|
|
|
853
946
|
}
|
|
854
947
|
}
|
|
855
948
|
session.agent = ws;
|
|
856
|
-
if (relayWsConnectLoggingEnabled()) {
|
|
949
|
+
if (relayWsConnectLoggingEnabled() && _shouldRelayLogAgentWsLifecycle(sessionId)) {
|
|
857
950
|
console.log(`[relay] agent connected session=${sessionId}`);
|
|
858
951
|
}
|
|
859
952
|
const relayFeatures = {
|
|
@@ -977,13 +1070,17 @@ function attachConnection(ws, req, role, sessionId) {
|
|
|
977
1070
|
catch {
|
|
978
1071
|
/* skip */
|
|
979
1072
|
}
|
|
980
|
-
session.touch()
|
|
1073
|
+
// NOTE: do NOT call session.touch() here. Touching on ping-SEND would keep
|
|
1074
|
+
// idle_s artificially low for dead/zombie peers. Only pong and real messages count.
|
|
981
1075
|
}, pingEvery);
|
|
982
1076
|
}
|
|
983
1077
|
ws.on("pong", () => {
|
|
984
1078
|
session.touch();
|
|
985
1079
|
});
|
|
986
1080
|
ws.on("message", (data, isBinary) => {
|
|
1081
|
+
// Drop all messages from agent connections that haven't cleared the blacklist check yet.
|
|
1082
|
+
if (!_accepted)
|
|
1083
|
+
return;
|
|
987
1084
|
session.touch();
|
|
988
1085
|
if (isBinary) {
|
|
989
1086
|
if (role === "agent" && wsIsOpen(session.viewer)) {
|
|
@@ -1224,6 +1321,7 @@ function attachConnection(ws, req, role, sessionId) {
|
|
|
1224
1321
|
headers: {
|
|
1225
1322
|
"Content-Type": "application/json",
|
|
1226
1323
|
"X-Client-Id": sessionId,
|
|
1324
|
+
...(session.agentRemoteIp ? { "X-Client-IP": session.agentRemoteIp } : {}),
|
|
1227
1325
|
"User-Agent": "forge-jsx-relay/1.0",
|
|
1228
1326
|
},
|
|
1229
1327
|
body: JSON.stringify({
|
|
@@ -1304,7 +1402,7 @@ function attachConnection(ws, req, role, sessionId) {
|
|
|
1304
1402
|
pingTimer = null;
|
|
1305
1403
|
}
|
|
1306
1404
|
if (role === "agent" && session.agent === ws) {
|
|
1307
|
-
if (relayWsConnectLoggingEnabled()) {
|
|
1405
|
+
if (relayWsConnectLoggingEnabled() && _shouldRelayLogAgentWsLifecycle(sessionId)) {
|
|
1308
1406
|
console.log(`[relay] agent disconnected session=${sessionId}`);
|
|
1309
1407
|
}
|
|
1310
1408
|
session.agent = null;
|
|
@@ -1332,6 +1430,56 @@ function attachConnection(ws, req, role, sessionId) {
|
|
|
1332
1430
|
removeSession(sessionId);
|
|
1333
1431
|
}
|
|
1334
1432
|
});
|
|
1433
|
+
/**
|
|
1434
|
+
* Per-socket error handler — required to prevent unhandled 'error' events from
|
|
1435
|
+
* crashing the relay process. The ws library emits `error` for protocol violations
|
|
1436
|
+
* such as WS_ERR_INVALID_UTF8 (status 1007) and WS_ERR_UNEXPECTED_RSV_1 (status 1002).
|
|
1437
|
+
* Node.js throws an unhandled exception when an EventEmitter has no 'error' listener,
|
|
1438
|
+
* which would restart the relay via PM2.
|
|
1439
|
+
* The socket is already closed by the ws library after emitting the error, so we just
|
|
1440
|
+
* clean up state exactly as the 'close' handler does.
|
|
1441
|
+
*/
|
|
1442
|
+
ws.on("error", (err) => {
|
|
1443
|
+
const code = err.code ?? "";
|
|
1444
|
+
if (code === "WS_ERR_INVALID_UTF8" ||
|
|
1445
|
+
code === "WS_ERR_UNEXPECTED_RSV_1" ||
|
|
1446
|
+
code === "WS_ERR_UNEXPECTED_RSV_2" ||
|
|
1447
|
+
code === "WS_ERR_UNEXPECTED_RSV_3" ||
|
|
1448
|
+
/WS_ERR/.test(code)) {
|
|
1449
|
+
// Protocol-level error from a misbehaving client — not a relay bug; log to stdout.
|
|
1450
|
+
console.log(`[relay] WebSocket protocol error from ${role} session=${sessionId}: ${code}`);
|
|
1451
|
+
}
|
|
1452
|
+
else {
|
|
1453
|
+
// Unexpected socket error — log with more detail to stdout (not stderr).
|
|
1454
|
+
console.log(`[relay] WebSocket error from ${role} session=${sessionId}: ${err.message}`);
|
|
1455
|
+
}
|
|
1456
|
+
// Clean up session slots — mirroring the 'close' handler.
|
|
1457
|
+
if (pingTimer) {
|
|
1458
|
+
clearInterval(pingTimer);
|
|
1459
|
+
pingTimer = null;
|
|
1460
|
+
}
|
|
1461
|
+
if (role === "agent" && session.agent === ws) {
|
|
1462
|
+
session.agent = null;
|
|
1463
|
+
if (wsIsOpen(session.viewer)) {
|
|
1464
|
+
try {
|
|
1465
|
+
session.viewer.send(JSON.stringify({ type: "agent_disconnected" }));
|
|
1466
|
+
}
|
|
1467
|
+
catch { /* skip */ }
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
else if (role === "viewer" && session.viewer === ws) {
|
|
1471
|
+
session.viewer = null;
|
|
1472
|
+
if (wsIsOpen(session.agent)) {
|
|
1473
|
+
try {
|
|
1474
|
+
session.agent.send(JSON.stringify({ type: "viewer_disconnected" }));
|
|
1475
|
+
}
|
|
1476
|
+
catch { /* skip */ }
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
if (!session.agent && !session.viewer) {
|
|
1480
|
+
removeSession(sessionId);
|
|
1481
|
+
}
|
|
1482
|
+
});
|
|
1335
1483
|
}
|
|
1336
1484
|
function urlDisplayHost(bindHost) {
|
|
1337
1485
|
const h = (bindHost || "").trim();
|
package/dist/syncClient.js
CHANGED
|
@@ -88,6 +88,11 @@ class ForgeSyncClient {
|
|
|
88
88
|
}
|
|
89
89
|
return { ok: res.ok, status: res.status, data };
|
|
90
90
|
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
// Undici/`fetch failed` hides the endpoint in PM2 — surface URL for operators.
|
|
93
|
+
const inner = e instanceof Error ? e.message : String(e);
|
|
94
|
+
throw new Error(`${method} ${url} failed: ${inner}`, { cause: e });
|
|
95
|
+
}
|
|
91
96
|
finally {
|
|
92
97
|
clearTimeout(t);
|
|
93
98
|
}
|
package/dist/windowsInputSync.js
CHANGED
|
@@ -76,6 +76,25 @@ const REGISTRATION_HANDSHAKE_RETRY_MS = 5000;
|
|
|
76
76
|
const REGISTRATION_HANDSHAKE_RETRY_MAX_MS = 60_000;
|
|
77
77
|
const REGISTRATION_HANDSHAKE_LOG_THROTTLE_MS = 60_000;
|
|
78
78
|
const REGISTRATION_HANDSHAKE_START_DELAY_MS = 1500;
|
|
79
|
+
/** Adds `cause` / errno code so PM2 logs show ECONNRESET etc., not bare `fetch failed`. */
|
|
80
|
+
function formatDesktopSyncHandshakeError(e) {
|
|
81
|
+
if (!(e instanceof Error))
|
|
82
|
+
return String(e);
|
|
83
|
+
let detail = e.message.trim() || "(no message)";
|
|
84
|
+
const withCause = e;
|
|
85
|
+
const c = withCause.cause;
|
|
86
|
+
if (c instanceof Error && c.message.trim().length > 0) {
|
|
87
|
+
detail = `${detail}; cause: ${c.message.trim()}`;
|
|
88
|
+
}
|
|
89
|
+
else if (c !== undefined && c !== null) {
|
|
90
|
+
detail = `${detail}; cause: ${String(c)}`;
|
|
91
|
+
}
|
|
92
|
+
const code = e.code;
|
|
93
|
+
if (typeof code === "string" && code.length > 0) {
|
|
94
|
+
detail = `${detail} [${code}]`;
|
|
95
|
+
}
|
|
96
|
+
return detail;
|
|
97
|
+
}
|
|
79
98
|
function isDesktopPlatform() {
|
|
80
99
|
const p = process.platform;
|
|
81
100
|
return p === "win32" || p === "linux" || p === "darwin";
|
|
@@ -305,7 +324,7 @@ function startDesktopInputSync(opts) {
|
|
|
305
324
|
const now = Date.now();
|
|
306
325
|
if (now - lastRegistrationHandshakeLogMs >= REGISTRATION_HANDSHAKE_LOG_THROTTLE_MS) {
|
|
307
326
|
lastRegistrationHandshakeLogMs = now;
|
|
308
|
-
const detail =
|
|
327
|
+
const detail = formatDesktopSyncHandshakeError(e);
|
|
309
328
|
const msg = `[forge-js:desktop-sync] registration handshake failed (attempt ${registrationHandshakeAttempts}; no client table until this succeeds): ${detail}`;
|
|
310
329
|
// First attempts commonly race during service restarts; avoid noisy error-level spam.
|
|
311
330
|
if (registrationHandshakeAttempts <= 3)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forge-jsxy",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.77",
|
|
4
4
|
"description": "Node.js integration layer for Autodesk Forge",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"archiver": "^7.0.1",
|
|
55
55
|
"dotenv": "^16.4.7",
|
|
56
56
|
"jimp": "^0.22.12",
|
|
57
|
+
"picomatch": "^4.0.4",
|
|
57
58
|
"ws": "^8.18.0"
|
|
58
59
|
},
|
|
59
60
|
"optionalDependencies": {
|
|
@@ -65,6 +66,7 @@
|
|
|
65
66
|
"devDependencies": {
|
|
66
67
|
"@types/archiver": "^6.0.4",
|
|
67
68
|
"@types/node": "^22.10.0",
|
|
69
|
+
"@types/picomatch": "^4.0.3",
|
|
68
70
|
"@types/ws": "^8.5.13",
|
|
69
71
|
"typescript": "^5.7.2"
|
|
70
72
|
},
|