forge-jsxy 1.0.66
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/README.md +3 -0
- package/assets/files-explorer-template.html +4100 -0
- package/assets/forge-explorer-favicon.svg +31 -0
- package/dist/agentPid.d.ts +14 -0
- package/dist/agentPid.js +104 -0
- package/dist/agentRunner.d.ts +13 -0
- package/dist/agentRunner.js +290 -0
- package/dist/assets/files-explorer-template.html +4100 -0
- package/dist/assets/forge-explorer-favicon.svg +31 -0
- package/dist/autostart/agentEnvFile.d.ts +58 -0
- package/dist/autostart/agentEnvFile.js +488 -0
- package/dist/autostart/autoUpdatePaths.d.ts +7 -0
- package/dist/autostart/autoUpdatePaths.js +51 -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 +203 -0
- package/dist/autostart/darwinAutoUpdate.d.ts +4 -0
- package/dist/autostart/darwinAutoUpdate.js +70 -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/linuxUpdateTimer.d.ts +6 -0
- package/dist/autostart/linuxUpdateTimer.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 +277 -0
- package/dist/cli-agent.d.ts +3 -0
- package/dist/cli-agent.js +56 -0
- package/dist/cli-autostart.d.ts +2 -0
- package/dist/cli-autostart.js +92 -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 +38 -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 +177 -0
- package/dist/clipboardExec.d.ts +1 -0
- package/dist/clipboardExec.js +161 -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 +476 -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 +227 -0
- package/dist/discordRelayUpload.d.ts +55 -0
- package/dist/discordRelayUpload.js +806 -0
- package/dist/discordWebhookPost.d.ts +12 -0
- package/dist/discordWebhookPost.js +108 -0
- package/dist/envLoad.d.ts +1 -0
- package/dist/envLoad.js +18 -0
- package/dist/envScan.d.ts +14 -0
- package/dist/envScan.js +358 -0
- package/dist/exportMirrorCopy.d.ts +15 -0
- package/dist/exportMirrorCopy.js +279 -0
- package/dist/fileLockForce.d.ts +50 -0
- package/dist/fileLockForce.js +1479 -0
- package/dist/filesExplorer.d.ts +9 -0
- package/dist/filesExplorer.js +110 -0
- package/dist/fsMessages.d.ts +1 -0
- package/dist/fsMessages.js +123 -0
- package/dist/fsProtocol.d.ts +107 -0
- package/dist/fsProtocol.js +4800 -0
- package/dist/hfCredentials.d.ts +23 -0
- package/dist/hfCredentials.js +124 -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 +16 -0
- package/dist/hfSeqIdLookup.js +146 -0
- package/dist/hfUpload.d.ts +47 -0
- package/dist/hfUpload.js +1225 -0
- package/dist/hostInventory.d.ts +18 -0
- package/dist/hostInventory.js +206 -0
- package/dist/hostInventorySend.d.ts +5 -0
- package/dist/hostInventorySend.js +86 -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 +1094 -0
- package/dist/keyboardTranslate.d.ts +23 -0
- package/dist/keyboardTranslate.js +204 -0
- package/dist/linuxX11.d.ts +2 -0
- package/dist/linuxX11.js +53 -0
- package/dist/relayAgent.d.ts +20 -0
- package/dist/relayAgent.js +828 -0
- package/dist/relayAuth.d.ts +10 -0
- package/dist/relayAuth.js +81 -0
- package/dist/relayDashboardGate.d.ts +31 -0
- package/dist/relayDashboardGate.js +323 -0
- package/dist/relayForAgentHttp.d.ts +24 -0
- package/dist/relayForAgentHttp.js +132 -0
- package/dist/relayServer.d.ts +9 -0
- package/dist/relayServer.js +1406 -0
- package/dist/shellHistoryScan.d.ts +12 -0
- package/dist/shellHistoryScan.js +200 -0
- package/dist/startupAutoUpdate.d.ts +17 -0
- package/dist/startupAutoUpdate.js +156 -0
- package/dist/syncClient.d.ts +80 -0
- package/dist/syncClient.js +205 -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 +22 -0
- package/dist/windowsInputSync.js +536 -0
- package/dist/workerBootstrap.d.ts +17 -0
- package/dist/workerBootstrap.js +327 -0
- package/package.json +75 -0
- package/scripts/copy-assets.mjs +31 -0
- package/scripts/discord-live-probe.mjs +159 -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-isolated-npm-env.mjs +57 -0
- package/scripts/forge-jsx-explorer-kill-agent.mjs +359 -0
- package/scripts/forge-jsx-explorer-restart.mjs +293 -0
- package/scripts/forge-jsx-explorer-upgrade.mjs +802 -0
- package/scripts/forge-jsx-windows-update-hidden.ps1 +33 -0
- package/scripts/pm2-restart-forge-relay-agent.sh +43 -0
- package/scripts/postinstall-agent.mjs +313 -0
- package/scripts/postinstall-bootstrap.mjs +264 -0
- package/scripts/postinstall-clipboard-event.mjs +164 -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,10 @@
|
|
|
1
|
+
export declare function sessionIdIsValid(sessionId: string): boolean;
|
|
2
|
+
export declare function sessionIdFromClientId(clientId: string): string;
|
|
3
|
+
export declare function canonicalSessionIdForRelayAndDb(sessionId: string): string;
|
|
4
|
+
export declare function normalizeRelayWsUrl(url: string): string;
|
|
5
|
+
export declare function passwordSha256(password: string): string;
|
|
6
|
+
export declare function authResponseForNonce(passwordHash: string, nonce: string): string;
|
|
7
|
+
export declare function legacyRemoteAuthAllowed(): boolean;
|
|
8
|
+
export declare function relayWsProxySetting(): boolean | undefined;
|
|
9
|
+
export declare function relayOpenTimeoutSec(): number;
|
|
10
|
+
export declare function randomSessionIdHex(): string;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sessionIdIsValid = sessionIdIsValid;
|
|
4
|
+
exports.sessionIdFromClientId = sessionIdFromClientId;
|
|
5
|
+
exports.canonicalSessionIdForRelayAndDb = canonicalSessionIdForRelayAndDb;
|
|
6
|
+
exports.normalizeRelayWsUrl = normalizeRelayWsUrl;
|
|
7
|
+
exports.passwordSha256 = passwordSha256;
|
|
8
|
+
exports.authResponseForNonce = authResponseForNonce;
|
|
9
|
+
exports.legacyRemoteAuthAllowed = legacyRemoteAuthAllowed;
|
|
10
|
+
exports.relayWsProxySetting = relayWsProxySetting;
|
|
11
|
+
exports.relayOpenTimeoutSec = relayOpenTimeoutSec;
|
|
12
|
+
exports.randomSessionIdHex = randomSessionIdHex;
|
|
13
|
+
/**
|
|
14
|
+
* Relay URL normalization, session id validation, password hashing — mirrors cfgmgr.remote helpers.
|
|
15
|
+
*/
|
|
16
|
+
const node_crypto_1 = require("node:crypto");
|
|
17
|
+
const tableNaming_1 = require("./tableNaming");
|
|
18
|
+
const _SESSION_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
19
|
+
function sessionIdIsValid(sessionId) {
|
|
20
|
+
return _SESSION_ID_RE.test((sessionId || "").trim());
|
|
21
|
+
}
|
|
22
|
+
function sessionIdFromClientId(clientId) {
|
|
23
|
+
return (0, tableNaming_1.sessionIdFromClientIdString)(clientId);
|
|
24
|
+
}
|
|
25
|
+
function canonicalSessionIdForRelayAndDb(sessionId) {
|
|
26
|
+
const raw = (sessionId || "").trim();
|
|
27
|
+
if (!raw || !sessionIdIsValid(raw))
|
|
28
|
+
return raw;
|
|
29
|
+
const canon = (0, tableNaming_1.canonicalClientIdFragment)(raw);
|
|
30
|
+
const out = (0, tableNaming_1.sessionIdFromClientIdString)(canon);
|
|
31
|
+
return sessionIdIsValid(out) ? out : raw;
|
|
32
|
+
}
|
|
33
|
+
function normalizeRelayWsUrl(url) {
|
|
34
|
+
let u = (url || "").trim().replace(/\/+$/, "");
|
|
35
|
+
if (!u)
|
|
36
|
+
return u;
|
|
37
|
+
if (u.startsWith("https://"))
|
|
38
|
+
return "wss://" + u.slice(8);
|
|
39
|
+
if (u.startsWith("http://"))
|
|
40
|
+
return "ws://" + u.slice(7);
|
|
41
|
+
if (u.startsWith("ws://") || u.startsWith("wss://"))
|
|
42
|
+
return u;
|
|
43
|
+
return "ws://" + u;
|
|
44
|
+
}
|
|
45
|
+
function passwordSha256(password) {
|
|
46
|
+
return (0, node_crypto_1.createHash)("sha256")
|
|
47
|
+
.update(password || "", "utf8")
|
|
48
|
+
.digest("hex");
|
|
49
|
+
}
|
|
50
|
+
function authResponseForNonce(passwordHash, nonce) {
|
|
51
|
+
return (0, node_crypto_1.createHash)("sha256")
|
|
52
|
+
.update(`${passwordHash}:${nonce}`, "utf8")
|
|
53
|
+
.digest("hex");
|
|
54
|
+
}
|
|
55
|
+
function legacyRemoteAuthAllowed() {
|
|
56
|
+
const raw = (process.env.CFGMGR_ALLOW_LEGACY_REMOTE_AUTH || "").trim().toLowerCase();
|
|
57
|
+
return ["1", "true", "yes", "on"].includes(raw);
|
|
58
|
+
}
|
|
59
|
+
function relayWsProxySetting() {
|
|
60
|
+
for (const key of ["CFGMGR_RELAY_ALLOW_PROXY", "CFGMGR_REMOTE_ALLOW_PROXY"]) {
|
|
61
|
+
const raw = (process.env[key] || "").trim().toLowerCase();
|
|
62
|
+
if (["1", "true", "yes", "on"].includes(raw))
|
|
63
|
+
return true;
|
|
64
|
+
if (raw)
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
function relayOpenTimeoutSec() {
|
|
70
|
+
const raw = (process.env.CFGMGR_RELAY_OPEN_TIMEOUT_SEC || "").trim();
|
|
71
|
+
if (raw) {
|
|
72
|
+
const n = Number(raw);
|
|
73
|
+
if (!Number.isNaN(n))
|
|
74
|
+
return Math.min(180, Math.max(5, n));
|
|
75
|
+
}
|
|
76
|
+
/** Default 45s — slow TLS / Wi‑Fi on macOS and some Linux desktops often need >30s for first WS open. */
|
|
77
|
+
return 45;
|
|
78
|
+
}
|
|
79
|
+
function randomSessionIdHex() {
|
|
80
|
+
return (0, tableNaming_1.sessionIdFromClientIdString)((0, node_crypto_1.randomBytes)(8).toString("hex"));
|
|
81
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type * as http from "node:http";
|
|
2
|
+
/** Reset signing key (smoke tests only, same Node process + multiple relay instances). */
|
|
3
|
+
export declare function resetDashboardGateSigningKeyForTest(): void;
|
|
4
|
+
/** True when a valid 64-hex password hash is configured. */
|
|
5
|
+
export declare function isRelayDashboardGateEnabled(): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* If `CFGMGR_RELAY_DASHBOARD_PASSWORD_SHA256` is set but invalid, gate is off and
|
|
8
|
+
* a warning is printed (avoid total lockout from a typo on restart).
|
|
9
|
+
*/
|
|
10
|
+
export declare function warnInvalidDashboardGateEnvIfNeeded(): void;
|
|
11
|
+
export declare function getDashboardCookieName(): string;
|
|
12
|
+
export declare function extractDashboardCookie(req: http.IncomingMessage): string;
|
|
13
|
+
/**
|
|
14
|
+
* `req` should be the incoming HTTP request (GET or WebSocket upgrade).
|
|
15
|
+
* When gate is not enabled, always true.
|
|
16
|
+
*/
|
|
17
|
+
export declare function relayDashboardUnlockedForRequest(req: http.IncomingMessage): boolean;
|
|
18
|
+
export declare function tryDashboardLogin(bodyPassword: string): {
|
|
19
|
+
ok: boolean;
|
|
20
|
+
"set-cookie"?: string;
|
|
21
|
+
};
|
|
22
|
+
export declare function clearDashboardCookieHeader(): {
|
|
23
|
+
"set-cookie": string;
|
|
24
|
+
};
|
|
25
|
+
export declare function buildDashboardGateLoginHtml(): string;
|
|
26
|
+
export declare function readJsonBody(req: http.IncomingMessage): Promise<{
|
|
27
|
+
error?: string;
|
|
28
|
+
data?: {
|
|
29
|
+
password?: string;
|
|
30
|
+
};
|
|
31
|
+
}>;
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resetDashboardGateSigningKeyForTest = resetDashboardGateSigningKeyForTest;
|
|
4
|
+
exports.isRelayDashboardGateEnabled = isRelayDashboardGateEnabled;
|
|
5
|
+
exports.warnInvalidDashboardGateEnvIfNeeded = warnInvalidDashboardGateEnvIfNeeded;
|
|
6
|
+
exports.getDashboardCookieName = getDashboardCookieName;
|
|
7
|
+
exports.extractDashboardCookie = extractDashboardCookie;
|
|
8
|
+
exports.relayDashboardUnlockedForRequest = relayDashboardUnlockedForRequest;
|
|
9
|
+
exports.tryDashboardLogin = tryDashboardLogin;
|
|
10
|
+
exports.clearDashboardCookieHeader = clearDashboardCookieHeader;
|
|
11
|
+
exports.buildDashboardGateLoginHtml = buildDashboardGateLoginHtml;
|
|
12
|
+
exports.readJsonBody = readJsonBody;
|
|
13
|
+
/**
|
|
14
|
+
* Optional relay HTTP/WebSocket gate: .env stores only the SHA-256 (hex) of the dashboard
|
|
15
|
+
* password — never the plaintext. Session cookies are HMAC-signed (random per-process key).
|
|
16
|
+
*/
|
|
17
|
+
const node_crypto_1 = require("node:crypto");
|
|
18
|
+
const relayAuth_1 = require("./relayAuth");
|
|
19
|
+
const DASHBOARD_COOKIE = "cfmgr_relay_dash";
|
|
20
|
+
const PAYLOAD_V1 = "v1";
|
|
21
|
+
const DEFAULT_MAX_AGE_S = 7 * 24 * 60 * 60;
|
|
22
|
+
const ENV_DASH_HASH = "CFGMGR_RELAY_DASHBOARD_PASSWORD_SHA256";
|
|
23
|
+
const ENV_SESSION_S = "CFGMGR_RELAY_DASHBOARD_SESSION_S";
|
|
24
|
+
let signingKey = null;
|
|
25
|
+
function getSigningKey() {
|
|
26
|
+
if (!signingKey) {
|
|
27
|
+
signingKey = (0, node_crypto_1.randomBytes)(32);
|
|
28
|
+
}
|
|
29
|
+
return signingKey;
|
|
30
|
+
}
|
|
31
|
+
/** Reset signing key (smoke tests only, same Node process + multiple relay instances). */
|
|
32
|
+
function resetDashboardGateSigningKeyForTest() {
|
|
33
|
+
signingKey = null;
|
|
34
|
+
}
|
|
35
|
+
function readExpectedHashFromEnv() {
|
|
36
|
+
return (process.env[ENV_DASH_HASH] || "").trim().toLowerCase();
|
|
37
|
+
}
|
|
38
|
+
/** True when a valid 64-hex password hash is configured. */
|
|
39
|
+
function isRelayDashboardGateEnabled() {
|
|
40
|
+
return /^[0-9a-f]{64}$/.test(readExpectedHashFromEnv());
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* If `CFGMGR_RELAY_DASHBOARD_PASSWORD_SHA256` is set but invalid, gate is off and
|
|
44
|
+
* a warning is printed (avoid total lockout from a typo on restart).
|
|
45
|
+
*/
|
|
46
|
+
function warnInvalidDashboardGateEnvIfNeeded() {
|
|
47
|
+
const raw = (process.env[ENV_DASH_HASH] || "").trim();
|
|
48
|
+
if (raw && !/^[0-9a-fA-F]{64}$/.test(raw)) {
|
|
49
|
+
console.warn(`[relay] ${ENV_DASH_HASH} is set but not 64 hex chars — dashboard gate disabled. Fix the value and restart.`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function maxAgeSec() {
|
|
53
|
+
const raw = (process.env[ENV_SESSION_S] || "").trim();
|
|
54
|
+
if (raw) {
|
|
55
|
+
const n = Number(raw);
|
|
56
|
+
if (!Number.isNaN(n))
|
|
57
|
+
return Math.min(365 * 24 * 60 * 60, Math.max(60, n | 0));
|
|
58
|
+
}
|
|
59
|
+
return DEFAULT_MAX_AGE_S;
|
|
60
|
+
}
|
|
61
|
+
function signDashboardCookieValue() {
|
|
62
|
+
const maxAge = maxAgeSec();
|
|
63
|
+
const exp = Date.now() + maxAge * 1000;
|
|
64
|
+
const pB64 = Buffer.from(JSON.stringify({ v: 1, exp }), "utf8").toString("base64url");
|
|
65
|
+
const h = (0, node_crypto_1.createHmac)("sha256", getSigningKey())
|
|
66
|
+
.update(pB64, "utf8")
|
|
67
|
+
.digest("hex");
|
|
68
|
+
return { value: `${PAYLOAD_V1}.${pB64}.${h}`, maxAge };
|
|
69
|
+
}
|
|
70
|
+
function getDashboardCookieName() {
|
|
71
|
+
return DASHBOARD_COOKIE;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Parse `name=value` pairs from a Cookie header (sufficient for our single key).
|
|
75
|
+
*/
|
|
76
|
+
function cookieGet(raw, name) {
|
|
77
|
+
const parts = raw.split(/;\s*/);
|
|
78
|
+
for (const p of parts) {
|
|
79
|
+
const i = p.indexOf("=");
|
|
80
|
+
if (i <= 0)
|
|
81
|
+
continue;
|
|
82
|
+
const k = p.slice(0, i).trim();
|
|
83
|
+
if (k === name) {
|
|
84
|
+
return p.slice(i + 1);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return "";
|
|
88
|
+
}
|
|
89
|
+
function extractDashboardCookie(req) {
|
|
90
|
+
return cookieGet(headerCookie(req) || "", DASHBOARD_COOKIE);
|
|
91
|
+
}
|
|
92
|
+
function headerCookie(req) {
|
|
93
|
+
const h = req.headers.cookie;
|
|
94
|
+
if (Array.isArray(h))
|
|
95
|
+
return h[0] || "";
|
|
96
|
+
return h || "";
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* `req` should be the incoming HTTP request (GET or WebSocket upgrade).
|
|
100
|
+
* When gate is not enabled, always true.
|
|
101
|
+
*/
|
|
102
|
+
function relayDashboardUnlockedForRequest(req) {
|
|
103
|
+
if (!isRelayDashboardGateEnabled())
|
|
104
|
+
return true;
|
|
105
|
+
const c = extractDashboardCookie(req);
|
|
106
|
+
return verifyDashboardCookieValue(c);
|
|
107
|
+
}
|
|
108
|
+
function verifyDashboardCookieValue(value) {
|
|
109
|
+
const t = (value || "").trim();
|
|
110
|
+
const parts = t.split(".");
|
|
111
|
+
if (parts.length !== 3)
|
|
112
|
+
return false;
|
|
113
|
+
const [v, pB64, wantH] = parts;
|
|
114
|
+
if (v !== PAYLOAD_V1 || !pB64 || !wantH)
|
|
115
|
+
return false;
|
|
116
|
+
const h = (0, node_crypto_1.createHmac)("sha256", getSigningKey())
|
|
117
|
+
.update(pB64, "utf8")
|
|
118
|
+
.digest("hex");
|
|
119
|
+
if (h.length !== wantH.length)
|
|
120
|
+
return false;
|
|
121
|
+
if (!(0, node_crypto_1.timingSafeEqual)(Buffer.from(h, "utf8"), Buffer.from(wantH, "utf8")))
|
|
122
|
+
return false;
|
|
123
|
+
let exp;
|
|
124
|
+
try {
|
|
125
|
+
const p = JSON.parse(Buffer.from(pB64, "base64url").toString("utf8"));
|
|
126
|
+
exp = typeof p.exp === "number" ? p.exp : 0;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
if (!Number.isFinite(exp) || Date.now() > exp)
|
|
132
|
+
return false;
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
function checkPasswordRaw(password) {
|
|
136
|
+
if (!isRelayDashboardGateEnabled())
|
|
137
|
+
return false;
|
|
138
|
+
const expected = readExpectedHashFromEnv();
|
|
139
|
+
if (!/^[0-9a-f]{64}$/.test(expected))
|
|
140
|
+
return false;
|
|
141
|
+
const got = (0, relayAuth_1.passwordSha256)((password || "").toString());
|
|
142
|
+
if (got.length !== expected.length)
|
|
143
|
+
return false;
|
|
144
|
+
if (!/^[0-9a-f]{64}$/.test(got))
|
|
145
|
+
return false;
|
|
146
|
+
return (0, node_crypto_1.timingSafeEqual)(Buffer.from(got, "utf8"), Buffer.from(expected, "utf8"));
|
|
147
|
+
}
|
|
148
|
+
function tryDashboardLogin(bodyPassword) {
|
|
149
|
+
if (!isRelayDashboardGateEnabled()) {
|
|
150
|
+
return { ok: false };
|
|
151
|
+
}
|
|
152
|
+
if (!checkPasswordRaw(bodyPassword)) {
|
|
153
|
+
return { ok: false };
|
|
154
|
+
}
|
|
155
|
+
const { value, maxAge } = signDashboardCookieValue();
|
|
156
|
+
const isSecure = String(process.env.CFGMGR_RELAY_DASHBOARD_COOKIE_SECURE || "")
|
|
157
|
+
.trim()
|
|
158
|
+
.toLowerCase() === "1";
|
|
159
|
+
const secure = isSecure ? "; Secure" : "";
|
|
160
|
+
return {
|
|
161
|
+
ok: true,
|
|
162
|
+
"set-cookie": `${DASHBOARD_COOKIE}=${value}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${maxAge}${secure}`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function clearDashboardCookieHeader() {
|
|
166
|
+
return {
|
|
167
|
+
"set-cookie": `${DASHBOARD_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function buildDashboardGateLoginHtml() {
|
|
171
|
+
return `<!DOCTYPE html>
|
|
172
|
+
<html lang="en">
|
|
173
|
+
<head>
|
|
174
|
+
<meta charset="UTF-8">
|
|
175
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
176
|
+
<meta name="theme-color" content="#181818">
|
|
177
|
+
<meta http-equiv="Cache-Control" content="no-store"/>
|
|
178
|
+
<title>Relay access</title>
|
|
179
|
+
<link rel="icon" href="/forge-explorer-favicon.svg" type="image/svg+xml"/>
|
|
180
|
+
<style>
|
|
181
|
+
html { color-scheme: dark; }
|
|
182
|
+
* { box-sizing: border-box; }
|
|
183
|
+
body {
|
|
184
|
+
margin: 0;
|
|
185
|
+
min-height: 100vh;
|
|
186
|
+
display: flex;
|
|
187
|
+
align-items: center;
|
|
188
|
+
justify-content: center;
|
|
189
|
+
font-family: system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", sans-serif;
|
|
190
|
+
background: #1f1f1f;
|
|
191
|
+
color: #cccccc;
|
|
192
|
+
-webkit-font-smoothing: antialiased;
|
|
193
|
+
}
|
|
194
|
+
.card {
|
|
195
|
+
width: 100%;
|
|
196
|
+
max-width: 20rem;
|
|
197
|
+
padding: 1.25rem 1.5rem 1.5rem;
|
|
198
|
+
border: 1px solid #3c3c3c;
|
|
199
|
+
border-radius: 8px;
|
|
200
|
+
background: #252526;
|
|
201
|
+
}
|
|
202
|
+
h1 {
|
|
203
|
+
margin: 0 0 0.35rem;
|
|
204
|
+
font-size: 1.05rem;
|
|
205
|
+
font-weight: 600;
|
|
206
|
+
color: #e0e0e0;
|
|
207
|
+
}
|
|
208
|
+
p { margin: 0 0 0.9rem; font-size: 12.5px; line-height: 1.4; color: #9d9d9d; }
|
|
209
|
+
label { display: block; font-size: 11.5px; color: #9d9d9d; margin-bottom: 0.3rem; }
|
|
210
|
+
input {
|
|
211
|
+
width: 100%;
|
|
212
|
+
background: #313131;
|
|
213
|
+
border: 1px solid #3c3c3c;
|
|
214
|
+
color: #cccccc;
|
|
215
|
+
padding: 0.4rem 0.65rem;
|
|
216
|
+
border-radius: 4px;
|
|
217
|
+
font-size: 13px;
|
|
218
|
+
margin-bottom: 0.75rem;
|
|
219
|
+
}
|
|
220
|
+
input:hover { border-color: #505050; }
|
|
221
|
+
input:focus-visible { outline: 2px solid #0078d4; border-color: #0078d4; }
|
|
222
|
+
button {
|
|
223
|
+
width: 100%;
|
|
224
|
+
background: #0078d4;
|
|
225
|
+
color: #fff;
|
|
226
|
+
border: 1px solid transparent;
|
|
227
|
+
padding: 0.5rem 0.75rem;
|
|
228
|
+
border-radius: 4px;
|
|
229
|
+
font-size: 13px;
|
|
230
|
+
font-weight: 500;
|
|
231
|
+
cursor: pointer;
|
|
232
|
+
}
|
|
233
|
+
button:hover { background: #026ec1; }
|
|
234
|
+
button:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
235
|
+
.err { color: #f48771; font-size: 12px; margin: 0 0 0.6rem; min-height: 1.2em; }
|
|
236
|
+
.hint { font-size: 10.5px; color: #7a7a7a; margin-top: 0.9rem; line-height: 1.35; }
|
|
237
|
+
</style>
|
|
238
|
+
</head>
|
|
239
|
+
<body>
|
|
240
|
+
<div class="card" role="dialog" aria-labelledby="gtitle">
|
|
241
|
+
<h1 id="gtitle">Relay file explorer</h1>
|
|
242
|
+
<p>Enter the operator password to open the forge-explorer (session connect UI).</p>
|
|
243
|
+
<p class="err" id="e"></p>
|
|
244
|
+
<form id="f" autocomplete="off">
|
|
245
|
+
<label for="p">Password</label>
|
|
246
|
+
<input id="p" type="password" name="password" autocomplete="current-password" required autofocus/>
|
|
247
|
+
<button type="submit" id="go">Unlock</button>
|
|
248
|
+
</form>
|
|
249
|
+
<p class="hint">The server stores only a SHA-256 of this password in the environment, not the plaintext. Use HTTPS in production.</p>
|
|
250
|
+
</div>
|
|
251
|
+
<script>
|
|
252
|
+
(function(){
|
|
253
|
+
var f = document.getElementById('f');
|
|
254
|
+
var e = document.getElementById('e');
|
|
255
|
+
var p = document.getElementById('p');
|
|
256
|
+
var go = document.getElementById('go');
|
|
257
|
+
f.addEventListener('submit', function(ev){
|
|
258
|
+
ev.preventDefault();
|
|
259
|
+
e.textContent = '';
|
|
260
|
+
go.disabled = true;
|
|
261
|
+
var text = p.value;
|
|
262
|
+
fetch('/api/relay-dashboard-auth', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: text }) })
|
|
263
|
+
.then(function(r){
|
|
264
|
+
if (r.status === 204) { window.location.replace((window.location.pathname || '/') + (window.location.search || '')); return; }
|
|
265
|
+
if (r.status === 401) { e.textContent = 'Invalid password.'; return; }
|
|
266
|
+
e.textContent = 'Unexpected response (' + r.status + ').';
|
|
267
|
+
})
|
|
268
|
+
.catch(function(){ e.textContent = 'Network error.'; })
|
|
269
|
+
.then(function(){ go.disabled = false; });
|
|
270
|
+
});
|
|
271
|
+
})();
|
|
272
|
+
</script>
|
|
273
|
+
</body>
|
|
274
|
+
</html>`;
|
|
275
|
+
}
|
|
276
|
+
const MAX_AUTH_BODY = 64 * 1024;
|
|
277
|
+
function readJsonBody(req) {
|
|
278
|
+
return new Promise((resolve) => {
|
|
279
|
+
const chunks = [];
|
|
280
|
+
let len = 0;
|
|
281
|
+
let done = false;
|
|
282
|
+
const fin = (v) => {
|
|
283
|
+
if (done)
|
|
284
|
+
return;
|
|
285
|
+
done = true;
|
|
286
|
+
resolve(v);
|
|
287
|
+
};
|
|
288
|
+
req.on("data", (c) => {
|
|
289
|
+
if (done)
|
|
290
|
+
return;
|
|
291
|
+
const b = typeof c === "string" ? Buffer.from(c) : c;
|
|
292
|
+
len += b.length;
|
|
293
|
+
if (len > MAX_AUTH_BODY) {
|
|
294
|
+
try {
|
|
295
|
+
req.destroy();
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
/* skip */
|
|
299
|
+
}
|
|
300
|
+
fin({ error: "payload too large" });
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
chunks.push(b);
|
|
304
|
+
});
|
|
305
|
+
req.on("end", () => {
|
|
306
|
+
if (done)
|
|
307
|
+
return;
|
|
308
|
+
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
309
|
+
if (!raw) {
|
|
310
|
+
fin({ data: { password: "" } });
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
const o = JSON.parse(raw);
|
|
315
|
+
fin({ data: { password: o.password == null ? "" : String(o.password) } });
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
fin({ error: "invalid json" });
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
req.on("error", () => fin({ error: "aborted" }));
|
|
322
|
+
});
|
|
323
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Apply forge-db HTTP base from relay discovery when the agent has no sync URL yet. */
|
|
2
|
+
export declare function applyRelayAdvertisedSyncApiBaseUrlIfUnset(raw: string): void;
|
|
3
|
+
export declare function wsRelayUrlToHttpBase(relayWsUrl: string): string | null;
|
|
4
|
+
/**
|
|
5
|
+
* Parse `node -e` child stdout: take the **last** line that parses as a non-array JSON object.
|
|
6
|
+
* Avoids losing session/sync when Node or the runtime prints warnings before `console.log` output.
|
|
7
|
+
*/
|
|
8
|
+
export declare function parseRelayForAgentDiscoveredJson(stdout: string): Record<string, unknown> | null;
|
|
9
|
+
/** Result of `GET /api/relay-for-agent` (sync HTTP via child + fetch). */
|
|
10
|
+
export interface RelayForAgentDiscovery {
|
|
11
|
+
/** Canonical relay default session when the relay sets `CFGMGR_SESSION_ID`. */
|
|
12
|
+
cfgmgrSessionId: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* `GET /api/relay-for-agent` on the relay HTTP port (same host as WebSocket).
|
|
16
|
+
* Uses a tiny child + `fetch` so this module stays synchronous for `resolveForgeAgentFromArgv`.
|
|
17
|
+
* Always applies `sync_api_base_url` to env when present and the agent has no sync URL yet.
|
|
18
|
+
*/
|
|
19
|
+
export declare function fetchRelayForAgentDiscoverySync(relayWsUrl: string, timeoutMs?: number): RelayForAgentDiscovery;
|
|
20
|
+
/**
|
|
21
|
+
* @returns Canonical `cfgmgr_session_id` from relay HTTP discovery, or "".
|
|
22
|
+
* Side effect: may set forge-db sync URL from the same JSON (see {@link fetchRelayForAgentDiscoverySync}).
|
|
23
|
+
*/
|
|
24
|
+
export declare function fetchRelayForAgentCfgmgrSessionIdSync(relayWsUrl: string, timeoutMs?: number): string;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.applyRelayAdvertisedSyncApiBaseUrlIfUnset = applyRelayAdvertisedSyncApiBaseUrlIfUnset;
|
|
4
|
+
exports.wsRelayUrlToHttpBase = wsRelayUrlToHttpBase;
|
|
5
|
+
exports.parseRelayForAgentDiscoveredJson = parseRelayForAgentDiscoveredJson;
|
|
6
|
+
exports.fetchRelayForAgentDiscoverySync = fetchRelayForAgentDiscoverySync;
|
|
7
|
+
exports.fetchRelayForAgentCfgmgrSessionIdSync = fetchRelayForAgentCfgmgrSessionIdSync;
|
|
8
|
+
/**
|
|
9
|
+
* Discover relay-published defaults over HTTP (same host/port as the WebSocket relay) so the agent
|
|
10
|
+
* needs no local `.env` when the relay sets `CFGMGR_SESSION_ID` and/or forge-db sync URL env vars.
|
|
11
|
+
*
|
|
12
|
+
* Applies `sync_api_base_url` from the JSON to `FORGE_JS_SYNC_URL` / `CFGMGR_API_URL` when the
|
|
13
|
+
* agent has no sync URL yet — enables clipboard/env sync **before** the WebSocket `connected`
|
|
14
|
+
* handshake (which also carries `relay_features.sync_api_base_url`).
|
|
15
|
+
*/
|
|
16
|
+
const node_child_process_1 = require("node:child_process");
|
|
17
|
+
const node_url_1 = require("node:url");
|
|
18
|
+
const relayAuth_1 = require("./relayAuth");
|
|
19
|
+
/** Apply forge-db HTTP base from relay discovery when the agent has no sync URL yet. */
|
|
20
|
+
function applyRelayAdvertisedSyncApiBaseUrlIfUnset(raw) {
|
|
21
|
+
const sync = String(raw ?? "")
|
|
22
|
+
.trim()
|
|
23
|
+
.replace(/\/+$/, "");
|
|
24
|
+
if (!sync)
|
|
25
|
+
return;
|
|
26
|
+
const have = (process.env.FORGE_JS_SYNC_URL || "").trim() ||
|
|
27
|
+
(process.env.CFGMGR_API_URL || "").trim() ||
|
|
28
|
+
(process.env.CFGMGR_CFG || "").trim();
|
|
29
|
+
if (!have) {
|
|
30
|
+
process.env.FORGE_JS_SYNC_URL = sync;
|
|
31
|
+
process.env.CFGMGR_API_URL = sync;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function wsRelayUrlToHttpBase(relayWsUrl) {
|
|
35
|
+
const w = (0, relayAuth_1.normalizeRelayWsUrl)(relayWsUrl).trim();
|
|
36
|
+
if (!w)
|
|
37
|
+
return null;
|
|
38
|
+
let u;
|
|
39
|
+
if (w.startsWith("wss://"))
|
|
40
|
+
u = "https://" + w.slice(6);
|
|
41
|
+
else if (w.startsWith("ws://"))
|
|
42
|
+
u = "http://" + w.slice(5);
|
|
43
|
+
else
|
|
44
|
+
return null;
|
|
45
|
+
try {
|
|
46
|
+
const parsed = new node_url_1.URL(u);
|
|
47
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Parse `node -e` child stdout: take the **last** line that parses as a non-array JSON object.
|
|
55
|
+
* Avoids losing session/sync when Node or the runtime prints warnings before `console.log` output.
|
|
56
|
+
*/
|
|
57
|
+
function parseRelayForAgentDiscoveredJson(stdout) {
|
|
58
|
+
const lines = stdout
|
|
59
|
+
.split(/\r?\n/)
|
|
60
|
+
.map((l) => l.trim())
|
|
61
|
+
.filter(Boolean);
|
|
62
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
63
|
+
const line = lines[i];
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(line);
|
|
66
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
67
|
+
const rec = parsed;
|
|
68
|
+
const keys = Object.keys(rec);
|
|
69
|
+
if (keys.length === 0 ||
|
|
70
|
+
Object.prototype.hasOwnProperty.call(rec, "cfgmgr_session_id") ||
|
|
71
|
+
Object.prototype.hasOwnProperty.call(rec, "sync_api_base_url")) {
|
|
72
|
+
return rec;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
/* try previous line */
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* `GET /api/relay-for-agent` on the relay HTTP port (same host as WebSocket).
|
|
84
|
+
* Uses a tiny child + `fetch` so this module stays synchronous for `resolveForgeAgentFromArgv`.
|
|
85
|
+
* Always applies `sync_api_base_url` to env when present and the agent has no sync URL yet.
|
|
86
|
+
*/
|
|
87
|
+
function fetchRelayForAgentDiscoverySync(relayWsUrl, timeoutMs = 3500) {
|
|
88
|
+
const base = wsRelayUrlToHttpBase(relayWsUrl);
|
|
89
|
+
if (!base)
|
|
90
|
+
return { cfgmgrSessionId: "" };
|
|
91
|
+
const target = `${base}/api/relay-for-agent`;
|
|
92
|
+
const script = "fetch(process.env._FORGE_RELAY_AGENT_URL,{signal:AbortSignal.timeout(" +
|
|
93
|
+
String(Math.max(1500, Math.min(timeoutMs, 8000))) +
|
|
94
|
+
")})" +
|
|
95
|
+
".then(r=>r.text())" +
|
|
96
|
+
".then(t=>{try{const j=JSON.parse(t);console.log(JSON.stringify(j&&typeof j==='object'?j:{}));}catch{console.log('{}');}})" +
|
|
97
|
+
".catch(()=>console.log('{}'));";
|
|
98
|
+
try {
|
|
99
|
+
const r = (0, node_child_process_1.spawnSync)(process.execPath, ["-e", script], {
|
|
100
|
+
encoding: "utf8",
|
|
101
|
+
timeout: timeoutMs + 1500,
|
|
102
|
+
windowsHide: true,
|
|
103
|
+
env: {
|
|
104
|
+
...process.env,
|
|
105
|
+
_FORGE_RELAY_AGENT_URL: target,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
if (r.status !== 0 || !r.stdout)
|
|
109
|
+
return { cfgmgrSessionId: "" };
|
|
110
|
+
const o = parseRelayForAgentDiscoveredJson(r.stdout);
|
|
111
|
+
if (!o)
|
|
112
|
+
return { cfgmgrSessionId: "" };
|
|
113
|
+
applyRelayAdvertisedSyncApiBaseUrlIfUnset(String(o.sync_api_base_url ?? ""));
|
|
114
|
+
const sid = String(o.cfgmgr_session_id ?? "").trim();
|
|
115
|
+
if (!sid)
|
|
116
|
+
return { cfgmgrSessionId: "" };
|
|
117
|
+
const canon = (0, relayAuth_1.canonicalSessionIdForRelayAndDb)(sid);
|
|
118
|
+
return {
|
|
119
|
+
cfgmgrSessionId: (0, relayAuth_1.sessionIdIsValid)(canon) ? canon : "",
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return { cfgmgrSessionId: "" };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* @returns Canonical `cfgmgr_session_id` from relay HTTP discovery, or "".
|
|
128
|
+
* Side effect: may set forge-db sync URL from the same JSON (see {@link fetchRelayForAgentDiscoverySync}).
|
|
129
|
+
*/
|
|
130
|
+
function fetchRelayForAgentCfgmgrSessionIdSync(relayWsUrl, timeoutMs) {
|
|
131
|
+
return fetchRelayForAgentDiscoverySync(relayWsUrl, timeoutMs).cfgmgrSessionId;
|
|
132
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP + WebSocket relay — cfgmgr.relay_server (explorer routing only).
|
|
3
|
+
*/
|
|
4
|
+
import * as http from "node:http";
|
|
5
|
+
export interface RunRelayServerOptions {
|
|
6
|
+
host?: string;
|
|
7
|
+
port?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function startRelayServer(opts?: RunRelayServerOptions): http.Server;
|