fraim 2.0.326 → 2.0.328
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/dist/src/api/payment/webhook.js +7 -6
- package/dist/src/cli/commands/first-run.js +1 -1
- package/dist/src/cli/commands/sync.js +11 -2
- package/dist/src/cli/doctor/checks/fraim-mcp-diagnostics.js +17 -10
- package/dist/src/cli/doctor/checks/mcp-connectivity-checks.js +20 -2
- package/dist/src/cli/mcp/fraim-mcp-latest-launcher.js +200 -130
- package/dist/src/cli/mcp/mcp-server-registry.js +1 -6
- package/dist/src/cli/utils/managed-agent-install.js +5 -1
- package/dist/src/cli/utils/managed-agent-paths.js +14 -3
- package/dist/src/cli/utils/managed-node-runtime.js +22 -19
- package/dist/src/cli/utils/persist-shell-path.js +121 -0
- package/dist/src/core/utils/redact-secrets.js +89 -0
- package/dist/src/first-run/session-service.js +25 -145
- package/dist/src/local-mcp-server/stdio-server.js +19 -6
- package/dist/src/middleware/auth.js +4 -3
- package/dist/src/middleware/logger.js +7 -24
- package/dist/src/middleware/telemetry.js +2 -1
- package/dist/src/services/installer-service.js +5 -47
- package/dist/src/services/session-service.js +2 -1
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.installManagedAgent = installManagedAgent;
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const persist_shell_path_1 = require("./persist-shell-path");
|
|
8
9
|
const path_1 = __importDefault(require("path"));
|
|
9
10
|
const managed_agent_paths_1 = require("./managed-agent-paths");
|
|
10
11
|
function errorMessage(error) {
|
|
@@ -104,7 +105,9 @@ async function runInstallWithStaleCleanupRetry(option, prefix, runInstall) {
|
|
|
104
105
|
// check before invoking this — it only covers the install-then-fallback path.
|
|
105
106
|
async function installManagedAgent(option, systemPath, deps) {
|
|
106
107
|
let standardInstallError = null;
|
|
107
|
-
const
|
|
108
|
+
const portableBin = (0, managed_agent_paths_1.getPortableNodeBinPath)();
|
|
109
|
+
// npm writes Windows shims at the prefix root, but POSIX executables in prefix/bin.
|
|
110
|
+
const prefix = process.platform === 'win32' ? portableBin : path_1.default.dirname(portableBin);
|
|
108
111
|
try {
|
|
109
112
|
await deps.runProcess('npm', ['install', '-g', option.installPackage], {
|
|
110
113
|
PATH: systemPath,
|
|
@@ -145,5 +148,6 @@ async function installManagedAgent(option, systemPath, deps) {
|
|
|
145
148
|
if (!ver) {
|
|
146
149
|
throw new Error(`${option.label} install completed, but the CLI is not runnable from FRAIM's managed PATH. Standard install failure: ${standardInstallError}`);
|
|
147
150
|
}
|
|
151
|
+
(0, persist_shell_path_1.persistShellPath)();
|
|
148
152
|
return { outcome: 'managed' };
|
|
149
153
|
}
|
|
@@ -36,12 +36,12 @@ Object.defineProperty(exports, "resolveMachinePathCached", { enumerable: true, g
|
|
|
36
36
|
function getManagedNodeRoot() {
|
|
37
37
|
return path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'node');
|
|
38
38
|
}
|
|
39
|
-
function getPortableNodeBinPath() {
|
|
39
|
+
function getPortableNodeBinPath(platform = process.platform, arch = process.arch) {
|
|
40
40
|
const nodeRoot = getManagedNodeRoot();
|
|
41
|
-
if (
|
|
41
|
+
if (platform === 'win32') {
|
|
42
42
|
if (fs_1.default.existsSync(nodeRoot)) {
|
|
43
43
|
const extractedDir = fs_1.default.readdirSync(nodeRoot, { withFileTypes: true })
|
|
44
|
-
.filter((entry) => entry.isDirectory() && entry.name.startsWith('node-v'))
|
|
44
|
+
.filter((entry) => entry.isDirectory() && entry.name.startsWith('node-v') && !entry.name.includes('.staging-'))
|
|
45
45
|
.sort((a, b) => b.name.localeCompare(a.name))[0];
|
|
46
46
|
if (extractedDir) {
|
|
47
47
|
return path_1.default.join(nodeRoot, extractedDir.name);
|
|
@@ -49,6 +49,17 @@ function getPortableNodeBinPath() {
|
|
|
49
49
|
}
|
|
50
50
|
return nodeRoot;
|
|
51
51
|
}
|
|
52
|
+
if (fs_1.default.existsSync(nodeRoot)) {
|
|
53
|
+
const versioned = fs_1.default.readdirSync(nodeRoot, { withFileTypes: true })
|
|
54
|
+
.filter(entry => entry.isDirectory() && /^node-v\d+\.\d+\.\d+-/.test(entry.name)
|
|
55
|
+
&& entry.name.endsWith(`-${platform}-${arch}`))
|
|
56
|
+
.sort((a, b) => b.name.localeCompare(a.name, undefined, { numeric: true }));
|
|
57
|
+
for (const entry of versioned) {
|
|
58
|
+
const bin = path_1.default.join(nodeRoot, entry.name, 'bin');
|
|
59
|
+
if (fs_1.default.existsSync(path_1.default.join(bin, 'node')) && fs_1.default.existsSync(path_1.default.join(bin, 'npm')))
|
|
60
|
+
return bin;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
52
63
|
return path_1.default.join(nodeRoot, 'bin');
|
|
53
64
|
}
|
|
54
65
|
function getManagedAgentBinDirs() {
|
|
@@ -17,16 +17,11 @@ const path_1 = __importDefault(require("path"));
|
|
|
17
17
|
const https_1 = __importDefault(require("https"));
|
|
18
18
|
const crypto_1 = __importDefault(require("crypto"));
|
|
19
19
|
const managed_agent_paths_1 = require("./managed-agent-paths");
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
// Electron's own bundled Node for FRAIM's own CLI/MCP shim, via ELECTRON_RUN_AS_NODE), and the
|
|
24
|
-
// separate CLI installer's portable-Node bootstrap (scripts/installer/fraim-install-win.template.cmd)
|
|
25
|
-
// never runs for a desktop-only user. That left two features silently unable to do their real job
|
|
26
|
-
// on such a machine: `hub-app-materializer.ts`'s npm install (this issue), and agent-CLI
|
|
27
|
-
// installation (`managed-agent-install.ts`), which also shells out to a bare `npm`.
|
|
20
|
+
// Desktop-only users may have no working system npm/npx. Installer setup, Hub
|
|
21
|
+
// materialization, first-run setup, and agent installation share this managed
|
|
22
|
+
// runtime resolver so each can prepare or reuse standalone Node and npm.
|
|
28
23
|
//
|
|
29
|
-
// This module is the one place that guarantees a real, usable `npm`/`node` exists
|
|
24
|
+
// This module is the one place that guarantees a real, usable `npm`/`node` exists for its
|
|
30
25
|
// consumers: prefer whatever is already on system PATH (common for technical users), then an
|
|
31
26
|
// already-downloaded managed runtime (shared with the CLI installer's `~/.fraim/node/` convention
|
|
32
27
|
// via `managed-agent-paths.ts` — reused, not duplicated, so a user who has ever run the `fraim` CLI
|
|
@@ -65,11 +60,14 @@ function findExecutableOnPath(name, pathValue = process.env.PATH) {
|
|
|
65
60
|
function hasWorkingNodeAndNpm(dir, platform = process.platform) {
|
|
66
61
|
return fs_1.default.existsSync(path_1.default.join(dir, npmExecutableName(platform))) && fs_1.default.existsSync(path_1.default.join(dir, nodeExecutableName(platform)));
|
|
67
62
|
}
|
|
68
|
-
|
|
63
|
+
function nodeBinDir(root, platform) {
|
|
64
|
+
return platform === 'win32' ? root : path_1.default.join(root, 'bin');
|
|
65
|
+
}
|
|
66
|
+
/** Whether `dir` (a versioned distribution root) holds a complete extraction for `version`. */
|
|
69
67
|
function isManagedNodeVersionComplete(dir, version, platform = process.platform) {
|
|
70
68
|
try {
|
|
71
69
|
const recorded = fs_1.default.readFileSync(path_1.default.join(dir, 'fraim-managed-node-version'), 'utf8').trim();
|
|
72
|
-
return recorded === version && hasWorkingNodeAndNpm(dir, platform);
|
|
70
|
+
return recorded === version && hasWorkingNodeAndNpm(nodeBinDir(dir, platform), platform);
|
|
73
71
|
}
|
|
74
72
|
catch {
|
|
75
73
|
return false;
|
|
@@ -99,6 +97,9 @@ function assertPathSafeNodeVersion(version) {
|
|
|
99
97
|
}
|
|
100
98
|
/** The exact folder name Node's own official archive extracts as — also this module's versioned subfolder name under the shared `~/.fraim/node/` root. */
|
|
101
99
|
function nodeVersionedDirName(request) {
|
|
100
|
+
if (!/^[a-z0-9_]+$/.test(request.arch)) {
|
|
101
|
+
throw new Error('Refusing to use an invalid managed Node.js architecture.');
|
|
102
|
+
}
|
|
102
103
|
return `node-v${assertPathSafeNodeVersion(request.version)}-${nodePlatformName(request.platform)}-${request.arch}`;
|
|
103
104
|
}
|
|
104
105
|
function nodeArchiveFileName(request) {
|
|
@@ -203,15 +204,16 @@ const defaultExtractNodeArchive = async (archivePath, destDir, request) => {
|
|
|
203
204
|
async function downloadAndPromoteVersionedNodeDir(args) {
|
|
204
205
|
const { nodeRoot, request, downloadArchive, extractArchive } = args;
|
|
205
206
|
const destDir = path_1.default.join(nodeRoot, nodeVersionedDirName(request));
|
|
206
|
-
const stagingDir = `${destDir}.staging-${process.pid}`;
|
|
207
207
|
fs_1.default.mkdirSync(nodeRoot, { recursive: true });
|
|
208
|
-
fs_1.default.rmSync(stagingDir, { recursive: true, force: true });
|
|
209
208
|
const archivePath = await downloadArchive(request);
|
|
209
|
+
let stagingDir;
|
|
210
210
|
try {
|
|
211
|
+
// tar requires an existing cwd. Each invocation owns a separate directory,
|
|
212
|
+
// including simultaneous requests within the same process.
|
|
213
|
+
stagingDir = fs_1.default.mkdtempSync(`${destDir}.staging-${process.pid}-`);
|
|
211
214
|
await extractArchive(archivePath, stagingDir, request);
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
// the archive's internal folder) — handle both without assuming either.
|
|
215
|
+
// Official tar and ZIP archives preserve their top-level distribution folder.
|
|
216
|
+
// Also accept a flattened root from a supplied extractor.
|
|
215
217
|
const nestedCandidate = path_1.default.join(stagingDir, nodeVersionedDirName(request));
|
|
216
218
|
const extractedRoot = fs_1.default.existsSync(nestedCandidate) ? nestedCandidate : stagingDir;
|
|
217
219
|
fs_1.default.writeFileSync(path_1.default.join(extractedRoot, 'fraim-managed-node-version'), request.version, 'utf8');
|
|
@@ -233,9 +235,10 @@ async function downloadAndPromoteVersionedNodeDir(args) {
|
|
|
233
235
|
}
|
|
234
236
|
finally {
|
|
235
237
|
fs_1.default.rmSync(archivePath, { force: true });
|
|
236
|
-
|
|
238
|
+
if (stagingDir)
|
|
239
|
+
fs_1.default.rmSync(stagingDir, { recursive: true, force: true });
|
|
237
240
|
}
|
|
238
|
-
return destDir;
|
|
241
|
+
return nodeBinDir(destDir, request.platform);
|
|
239
242
|
}
|
|
240
243
|
/**
|
|
241
244
|
* Resolve a working `npm` executable, preferring (in order): system PATH, an already-downloaded
|
|
@@ -252,7 +255,7 @@ async function ensureManagedNpm(options = {}) {
|
|
|
252
255
|
if (systemNpm) {
|
|
253
256
|
return { npmPath: systemNpm, binDir: path_1.default.dirname(systemNpm), source: 'system' };
|
|
254
257
|
}
|
|
255
|
-
const existingBinDir = (0, managed_agent_paths_1.getPortableNodeBinPath)();
|
|
258
|
+
const existingBinDir = (0, managed_agent_paths_1.getPortableNodeBinPath)(platform, arch);
|
|
256
259
|
if (hasWorkingNodeAndNpm(existingBinDir, platform)) {
|
|
257
260
|
return { npmPath: path_1.default.join(existingBinDir, npmExecutableName(platform)), binDir: existingBinDir, source: 'managed-existing' };
|
|
258
261
|
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.buildPersistShellPathWindowsCommand = buildPersistShellPathWindowsCommand;
|
|
7
|
+
exports.persistShellPath = persistShellPath;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const os_1 = __importDefault(require("os"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const child_process_1 = require("child_process");
|
|
12
|
+
const script_sync_utils_1 = require("./script-sync-utils");
|
|
13
|
+
const managed_agent_paths_1 = require("./managed-agent-paths");
|
|
14
|
+
// Issue #1285: pure command-string builder extracted from persistShellPath()
|
|
15
|
+
// so the generated PowerShell command's directory order (the current
|
|
16
|
+
// versioned dir added before the legacy flat dir, per `bins`' own order —
|
|
17
|
+
// see getManagedAgentBinDirs()) can be asserted directly, without mutating
|
|
18
|
+
// the real registry-backed Windows User PATH in a test.
|
|
19
|
+
function buildPersistShellPathWindowsCommand(bins) {
|
|
20
|
+
const assignments = bins.map((entry, index) => `$bin${index} = '${entry.replace(/'/g, "''")}'`);
|
|
21
|
+
const removals = bins.map((_, index) => `$parts = @($parts | Where-Object { $_ -ne $bin${index} })`);
|
|
22
|
+
const additions = bins.map((_, index) => `$parts += $bin${index}`);
|
|
23
|
+
return [
|
|
24
|
+
`$ErrorActionPreference = 'Stop'`,
|
|
25
|
+
...assignments,
|
|
26
|
+
`$cur = [Environment]::GetEnvironmentVariable('PATH', 'User')`,
|
|
27
|
+
`$parts = @($cur -split ';' | Where-Object { $_ })`,
|
|
28
|
+
...removals,
|
|
29
|
+
...additions,
|
|
30
|
+
`$cur = ($parts | Select-Object -Unique) -join ';'`,
|
|
31
|
+
`[Environment]::SetEnvironmentVariable('PATH', $cur, 'User')`,
|
|
32
|
+
].join('; ');
|
|
33
|
+
}
|
|
34
|
+
function persistShellPath() {
|
|
35
|
+
const marker = '# FRAIM managed binaries';
|
|
36
|
+
const legacyExportLine = 'export PATH=”$HOME/.fraim/node/bin:$PATH”';
|
|
37
|
+
// The stable CLI shim must outrank old prefix installs already on inherited PATH.
|
|
38
|
+
// Agent bins still follow the user's PATH, preserving system-agent precedence.
|
|
39
|
+
const bins = [path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'bin'), ...(0, managed_agent_paths_1.getManagedAgentBinDirs)()];
|
|
40
|
+
const shellBins = bins.map(bin => {
|
|
41
|
+
const relative = path_1.default.relative((0, script_sync_utils_1.getUserFraimDir)(), bin).split(path_1.default.sep).join('/');
|
|
42
|
+
return '$HOME/.fraim/' + relative.replace(/[\\"$`]/g, '\\$&');
|
|
43
|
+
});
|
|
44
|
+
const exportLine = `export PATH="${shellBins[0]}:$PATH:${shellBins.slice(1).join(':')}"`;
|
|
45
|
+
// Guard against editor-autocorrect curly-quote regression (issue #1688):
|
|
46
|
+
// U+201C/201D/2018/2019 in exportLine would be written verbatim to shell
|
|
47
|
+
// profiles and break PATH resolution for all pre-existing PATH entries.
|
|
48
|
+
if (/[“”‘’]/.test(exportLine)) {
|
|
49
|
+
throw new Error(`persistShellPath: exportLine contains curly quotes. Writing this to a shell profile would corrupt PATH resolution. Check for editor autocorrect in persist-shell-path.ts. Line: ${JSON.stringify(exportLine)}`);
|
|
50
|
+
}
|
|
51
|
+
const stanza = `\n${marker}\n${exportLine}\n`;
|
|
52
|
+
if (process.platform === 'win32') {
|
|
53
|
+
const psCmd = buildPersistShellPathWindowsCommand(bins);
|
|
54
|
+
// Issue #1692: a sandboxed test run (row detection on load, node row
|
|
55
|
+
// success, fraim row success all reach this function) must never write
|
|
56
|
+
// its temp fake-home bin dirs into the real, registry-backed Windows
|
|
57
|
+
// User PATH. FRAIM_TEST_SANDBOX is already the canonical "this process
|
|
58
|
+
// is a sandboxed test run" signal set by every test runner (see
|
|
59
|
+
// getUserFraimDirPath() in project-fraim-paths.ts) rather than a
|
|
60
|
+
// dedicated flag for this call site.
|
|
61
|
+
if (process.env.FRAIM_TEST_SANDBOX !== '1') {
|
|
62
|
+
const result = (0, child_process_1.spawnSync)('powershell', ['-NoProfile', '-NonInteractive', '-Command', psCmd], { encoding: 'utf8', timeout: 10000 });
|
|
63
|
+
if (result.error || result.status !== 0) {
|
|
64
|
+
throw new Error(`Could not register managed binaries on the User PATH: ${result.error?.message || result.stderr}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Fall through to write POSIX profile files for Git Bash (VS Code
|
|
68
|
+
// integrated terminal, Claude Code, etc.) which do not inherit the
|
|
69
|
+
// Windows user PATH written above.
|
|
70
|
+
}
|
|
71
|
+
// Test runners isolate FRAIM_USER_DIR but may leave os.homedir() unchanged.
|
|
72
|
+
const sandboxDir = process.env.FRAIM_USER_DIR ? path_1.default.resolve(process.env.FRAIM_USER_DIR) : null;
|
|
73
|
+
const profileHome = process.env.FRAIM_TEST_SANDBOX === '1'
|
|
74
|
+
? (sandboxDir && (path_1.default.basename(sandboxDir) === '.fraim' ? path_1.default.dirname(sandboxDir) : sandboxDir))
|
|
75
|
+
: os_1.default.homedir();
|
|
76
|
+
if (!profileHome)
|
|
77
|
+
return;
|
|
78
|
+
fs_1.default.mkdirSync(profileHome, { recursive: true });
|
|
79
|
+
const profiles = ['.zshrc', '.bash_profile', '.bashrc'].map(name => path_1.default.join(profileHome, name));
|
|
80
|
+
// macOS defaults to zsh since Catalina — create .zshrc if it doesn't exist.
|
|
81
|
+
if (process.platform === 'darwin' && !fs_1.default.existsSync(profiles[0])) {
|
|
82
|
+
fs_1.default.writeFileSync(profiles[0], stanza, 'utf8');
|
|
83
|
+
}
|
|
84
|
+
// On Windows, Git Bash reads .bash_profile (login) and .bashrc (interactive).
|
|
85
|
+
// Neither is created by the OS or Git Bash installer, so create .bashrc if
|
|
86
|
+
// neither exists — otherwise the PATH entry is never written and Git Bash
|
|
87
|
+
// (VS Code integrated terminal, Claude Code) never sees managed node dirs.
|
|
88
|
+
if (process.platform !== 'darwin') {
|
|
89
|
+
const bashProfile = profiles[1]; // .bash_profile
|
|
90
|
+
const bashrc = profiles[2]; // .bashrc
|
|
91
|
+
if (!fs_1.default.existsSync(bashProfile) && !fs_1.default.existsSync(bashrc)) {
|
|
92
|
+
fs_1.default.writeFileSync(bashrc, stanza, 'utf8');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
for (const profile of profiles) {
|
|
96
|
+
// .zshrc is macOS/Linux only; Git Bash on Windows uses .bash_profile/.bashrc.
|
|
97
|
+
if (process.platform === 'win32' && profile.endsWith('.zshrc'))
|
|
98
|
+
continue;
|
|
99
|
+
try {
|
|
100
|
+
if (!fs_1.default.existsSync(profile))
|
|
101
|
+
continue;
|
|
102
|
+
const content = fs_1.default.readFileSync(profile, 'utf8');
|
|
103
|
+
if (content.includes(legacyExportLine)) {
|
|
104
|
+
fs_1.default.writeFileSync(profile, content.replace(legacyExportLine, exportLine), 'utf8');
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (content.includes(marker)) {
|
|
108
|
+
if (!content.includes(exportLine)) {
|
|
109
|
+
// Replace stale runtime directories or an older shim ordering.
|
|
110
|
+
fs_1.default.writeFileSync(profile, content.replace(/export PATH=[\u201D"][^\u201D"]*\.fraim[^\u201D"]*[\u201D"]/, exportLine), 'utf8');
|
|
111
|
+
}
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
fs_1.default.appendFileSync(profile, stanza, 'utf8');
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Ignore unreadable or missing profiles.
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
(0, managed_agent_paths_1.invalidateMachinePathCache)();
|
|
121
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Credential redaction for log sinks (issue #1701, SEC-LEAK `63bc2c102800`).
|
|
4
|
+
*
|
|
5
|
+
* A FRAIM API key is a bearer credential: anything that writes one to a console,
|
|
6
|
+
* a log file, or a structured log record has disclosed it to everyone who can
|
|
7
|
+
* read that sink. Every log statement that wants to identify a caller must go
|
|
8
|
+
* through `redactApiKey` (for a known key) or `apiKeyRefFromRequest` (for an
|
|
9
|
+
* HTTP request) instead of interpolating the key itself.
|
|
10
|
+
*
|
|
11
|
+
* The reference is an unsalted SHA-256 prefix rather than a last-four suffix:
|
|
12
|
+
* - it leaks no key material at all, where a suffix leaks 16 bits;
|
|
13
|
+
* - it is stable, so the same caller correlates across the server log, the MCP
|
|
14
|
+
* error log, and the local proxy's own log on the user's machine;
|
|
15
|
+
* - it is unsalted deliberately, because the local proxy runs in a different
|
|
16
|
+
* process from the server and must derive the same reference without sharing
|
|
17
|
+
* a secret. Keys carry 128 bits of entropy (`generateApiKey` in
|
|
18
|
+
* `src/fraim/db-service.ts`), so the digest is not brute-forceable.
|
|
19
|
+
*
|
|
20
|
+
* Fingerprinting every value, including one an unauthenticated caller supplied,
|
|
21
|
+
* also removes CRLF log-forging: no caller-controlled bytes reach a log line.
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.NO_CREDENTIAL_REF = void 0;
|
|
25
|
+
exports.redactApiKey = redactApiKey;
|
|
26
|
+
exports.redactSecretsInText = redactSecretsInText;
|
|
27
|
+
exports.apiKeyRefFromRequest = apiKeyRefFromRequest;
|
|
28
|
+
const crypto_1 = require("crypto");
|
|
29
|
+
/** Prefix every FRAIM-issued API key carries. */
|
|
30
|
+
const FRAIM_KEY_PREFIX = 'fraim_';
|
|
31
|
+
/** Hex characters kept from the digest. 48 bits: collision-free at any realistic key population. */
|
|
32
|
+
const REFERENCE_HEX_CHARS = 12;
|
|
33
|
+
/** What a log line shows when the request carried no credential at all. */
|
|
34
|
+
exports.NO_CREDENTIAL_REF = 'key_none';
|
|
35
|
+
/**
|
|
36
|
+
* A FRAIM key embedded in free-form text. Matched loosely on the character class
|
|
37
|
+
* so a truncated, uppercased, or future-format key is still caught.
|
|
38
|
+
*/
|
|
39
|
+
const EMBEDDED_FRAIM_KEY = /fraim_[A-Za-z0-9]{16,}/g;
|
|
40
|
+
/**
|
|
41
|
+
* Non-reversible, stable reference for an API key, safe to write to any log sink.
|
|
42
|
+
* Returns `NO_CREDENTIAL_REF` when there is no credential to reference.
|
|
43
|
+
*/
|
|
44
|
+
function redactApiKey(value) {
|
|
45
|
+
if (typeof value !== 'string')
|
|
46
|
+
return exports.NO_CREDENTIAL_REF;
|
|
47
|
+
const trimmed = value.trim();
|
|
48
|
+
if (trimmed.length === 0)
|
|
49
|
+
return exports.NO_CREDENTIAL_REF;
|
|
50
|
+
const digest = (0, crypto_1.createHash)('sha256').update(trimmed, 'utf8').digest('hex');
|
|
51
|
+
return `key_${digest.slice(0, REFERENCE_HEX_CHARS)}`;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Replace every credential-shaped value embedded in free-form text with its
|
|
55
|
+
* reference. Use on text that is logged but not authored here: request bodies,
|
|
56
|
+
* error messages, and stack traces. Idempotent.
|
|
57
|
+
*/
|
|
58
|
+
function redactSecretsInText(text) {
|
|
59
|
+
if (typeof text !== 'string' || !text.includes(FRAIM_KEY_PREFIX))
|
|
60
|
+
return text;
|
|
61
|
+
return text.replace(EMBEDDED_FRAIM_KEY, (match) => redactApiKey(match));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Reference for whichever credential a request carried, in precedence order:
|
|
65
|
+
* the value auth resolved, then the `x-api-key` header, then the `api-key` query
|
|
66
|
+
* parameter. The authenticated value wins so a forged header cannot rewrite the
|
|
67
|
+
* identity a log line reports.
|
|
68
|
+
*
|
|
69
|
+
* Shared by the request logger and the MCP error handler so both report the same
|
|
70
|
+
* reference for the same caller.
|
|
71
|
+
*/
|
|
72
|
+
function apiKeyRefFromRequest(req) {
|
|
73
|
+
if (!req)
|
|
74
|
+
return exports.NO_CREDENTIAL_REF;
|
|
75
|
+
const fromContext = req.apiKeyData?.key;
|
|
76
|
+
if (typeof fromContext === 'string' && fromContext.trim().length > 0) {
|
|
77
|
+
return redactApiKey(fromContext);
|
|
78
|
+
}
|
|
79
|
+
const headerValue = req.headers?.['x-api-key'];
|
|
80
|
+
const fromHeader = Array.isArray(headerValue) ? headerValue[0] : headerValue;
|
|
81
|
+
if (typeof fromHeader === 'string' && fromHeader.trim().length > 0) {
|
|
82
|
+
return redactApiKey(fromHeader);
|
|
83
|
+
}
|
|
84
|
+
const fromQuery = req.query?.['api-key'];
|
|
85
|
+
if (typeof fromQuery === 'string' && fromQuery.trim().length > 0) {
|
|
86
|
+
return redactApiKey(fromQuery);
|
|
87
|
+
}
|
|
88
|
+
return exports.NO_CREDENTIAL_REF;
|
|
89
|
+
}
|
|
@@ -38,7 +38,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.FIRST_RUN_ROW_IDS = exports.FirstRunSessionService = void 0;
|
|
40
40
|
exports.commandVersion = commandVersion;
|
|
41
|
-
exports.buildPersistShellPathWindowsCommand = buildPersistShellPathWindowsCommand;
|
|
42
41
|
exports.buildFirstRunHubLaunchArgs = buildFirstRunHubLaunchArgs;
|
|
43
42
|
const fs_1 = __importDefault(require("fs"));
|
|
44
43
|
const os_1 = __importDefault(require("os"));
|
|
@@ -52,6 +51,7 @@ const setup_1 = require("../cli/commands/setup");
|
|
|
52
51
|
const fraim_mcp_latest_launcher_1 = require("../cli/mcp/fraim-mcp-latest-launcher");
|
|
53
52
|
const script_sync_utils_1 = require("../cli/utils/script-sync-utils");
|
|
54
53
|
const managed_agent_paths_1 = require("../cli/utils/managed-agent-paths");
|
|
54
|
+
const persist_shell_path_1 = require("../cli/utils/persist-shell-path");
|
|
55
55
|
const managed_agent_install_1 = require("../cli/utils/managed-agent-install");
|
|
56
56
|
const managed_node_runtime_1 = require("../cli/utils/managed-node-runtime");
|
|
57
57
|
const types_1 = require("./types");
|
|
@@ -109,120 +109,6 @@ function ensureOutputDirs() {
|
|
|
109
109
|
appendInstallLog(`orphaned-managed-shims-removed ${removedShims.join(', ')}`);
|
|
110
110
|
}
|
|
111
111
|
})();
|
|
112
|
-
// Issue #1285: pure command-string builder extracted from persistShellPath()
|
|
113
|
-
// so the generated PowerShell command's directory order (the current
|
|
114
|
-
// versioned dir added before the legacy flat dir, per `bins`' own order —
|
|
115
|
-
// see getManagedAgentBinDirs()) can be asserted directly, without mutating
|
|
116
|
-
// the real registry-backed Windows User PATH in a test.
|
|
117
|
-
function buildPersistShellPathWindowsCommand(bins) {
|
|
118
|
-
const assignments = bins.map((entry, index) => `$bin${index} = '${entry.replace(/'/g, "''")}'`);
|
|
119
|
-
const removals = bins.map((_, index) => `$parts = @($parts | Where-Object { $_ -ne $bin${index} })`);
|
|
120
|
-
const additions = bins.map((_, index) => `$parts += $bin${index}`);
|
|
121
|
-
return [
|
|
122
|
-
...assignments,
|
|
123
|
-
`$cur = [Environment]::GetEnvironmentVariable('PATH', 'User')`,
|
|
124
|
-
`$parts = @($cur -split ';' | Where-Object { $_ })`,
|
|
125
|
-
...removals,
|
|
126
|
-
...additions,
|
|
127
|
-
`$cur = ($parts | Select-Object -Unique) -join ';'`,
|
|
128
|
-
`[Environment]::SetEnvironmentVariable('PATH', $cur, 'User')`,
|
|
129
|
-
].join('; ');
|
|
130
|
-
}
|
|
131
|
-
function persistShellPath(includePackagedFraimBin = false) {
|
|
132
|
-
const marker = '# FRAIM managed binaries';
|
|
133
|
-
const legacyExportLine = 'export PATH=”$HOME/.fraim/node/bin:$PATH”';
|
|
134
|
-
// On Windows/Git Bash: add both the flat dir (bare POSIX agent scripts npm
|
|
135
|
-
// generates) and the versioned node subdir (node.exe). $HOME in Git Bash
|
|
136
|
-
// resolves to the Windows home directory, matching the registry paths.
|
|
137
|
-
let exportLine;
|
|
138
|
-
if (process.platform === 'win32') {
|
|
139
|
-
const portableNodeBin = (0, managed_agent_paths_1.getPortableNodeBinPath)();
|
|
140
|
-
const nodeRoot = (0, managed_agent_paths_1.getManagedNodeRoot)();
|
|
141
|
-
const fraimBinEntry = includePackagedFraimBin ? ':$HOME/.fraim/bin' : '';
|
|
142
|
-
// Versioned dir first (has fresh installs, matches getManagedAgentBinDirs()
|
|
143
|
-
// order), flat dir second (retains bare POSIX agent scripts as fallback).
|
|
144
|
-
exportLine = portableNodeBin !== nodeRoot
|
|
145
|
-
? `export PATH="$PATH:$HOME/.fraim/node/${path_1.default.basename(portableNodeBin)}:$HOME/.fraim/node${fraimBinEntry}"`
|
|
146
|
-
: `export PATH="$PATH:$HOME/.fraim/node${fraimBinEntry}"`;
|
|
147
|
-
}
|
|
148
|
-
else {
|
|
149
|
-
exportLine = includePackagedFraimBin
|
|
150
|
-
? 'export PATH="$PATH:$HOME/.fraim/bin:$HOME/.fraim/node/bin"'
|
|
151
|
-
: 'export PATH="$PATH:$HOME/.fraim/node/bin"';
|
|
152
|
-
}
|
|
153
|
-
// Guard against editor-autocorrect curly-quote regression (issue #1688):
|
|
154
|
-
// U+201C/201D/2018/2019 in exportLine would be written verbatim to shell
|
|
155
|
-
// profiles and break PATH resolution for all pre-existing PATH entries.
|
|
156
|
-
if (/[“”‘’]/.test(exportLine)) {
|
|
157
|
-
throw new Error(`persistShellPath: exportLine contains curly quotes. Writing this to a shell profile would corrupt PATH resolution. Check for editor autocorrect in session-service.ts. Line: ${JSON.stringify(exportLine)}`);
|
|
158
|
-
}
|
|
159
|
-
const stanza = `\n${marker}\n${exportLine}\n`;
|
|
160
|
-
if (process.platform === 'win32') {
|
|
161
|
-
const bins = includePackagedFraimBin
|
|
162
|
-
? [path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'bin'), ...(0, managed_agent_paths_1.getManagedAgentBinDirs)()]
|
|
163
|
-
: (0, managed_agent_paths_1.getManagedAgentBinDirs)();
|
|
164
|
-
const psCmd = buildPersistShellPathWindowsCommand(bins);
|
|
165
|
-
// Issue #1692: a sandboxed test run (row detection on load, node row
|
|
166
|
-
// success, fraim row success all reach this function) must never write
|
|
167
|
-
// its temp fake-home bin dirs into the real, registry-backed Windows
|
|
168
|
-
// User PATH. FRAIM_TEST_SANDBOX is already the canonical "this process
|
|
169
|
-
// is a sandboxed test run" signal set by every test runner (see
|
|
170
|
-
// getUserFraimDirPath() in project-fraim-paths.ts) rather than a
|
|
171
|
-
// dedicated flag for this call site.
|
|
172
|
-
if (process.env.FRAIM_TEST_SANDBOX !== '1') {
|
|
173
|
-
(0, child_process_1.spawnSync)('powershell', ['-NoProfile', '-NonInteractive', '-Command', psCmd], { encoding: 'utf8' });
|
|
174
|
-
}
|
|
175
|
-
// Fall through to write POSIX profile files for Git Bash (VS Code
|
|
176
|
-
// integrated terminal, Claude Code, etc.) which do not inherit the
|
|
177
|
-
// Windows user PATH written above.
|
|
178
|
-
}
|
|
179
|
-
const profiles = [
|
|
180
|
-
path_1.default.join(os_1.default.homedir(), '.zshrc'),
|
|
181
|
-
path_1.default.join(os_1.default.homedir(), '.bash_profile'),
|
|
182
|
-
path_1.default.join(os_1.default.homedir(), '.bashrc'),
|
|
183
|
-
];
|
|
184
|
-
// macOS defaults to zsh since Catalina — create .zshrc if it doesn't exist.
|
|
185
|
-
if (process.platform === 'darwin' && !fs_1.default.existsSync(profiles[0])) {
|
|
186
|
-
fs_1.default.writeFileSync(profiles[0], stanza, 'utf8');
|
|
187
|
-
}
|
|
188
|
-
// On Windows, Git Bash reads .bash_profile (login) and .bashrc (interactive).
|
|
189
|
-
// Neither is created by the OS or Git Bash installer, so create .bashrc if
|
|
190
|
-
// neither exists — otherwise the PATH entry is never written and Git Bash
|
|
191
|
-
// (VS Code integrated terminal, Claude Code) never sees managed node dirs.
|
|
192
|
-
if (process.platform === 'win32') {
|
|
193
|
-
const bashProfile = profiles[1]; // .bash_profile
|
|
194
|
-
const bashrc = profiles[2]; // .bashrc
|
|
195
|
-
if (!fs_1.default.existsSync(bashProfile) && !fs_1.default.existsSync(bashrc)) {
|
|
196
|
-
fs_1.default.writeFileSync(bashrc, stanza, 'utf8');
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
for (const profile of profiles) {
|
|
200
|
-
// .zshrc is macOS/Linux only; Git Bash on Windows uses .bash_profile/.bashrc.
|
|
201
|
-
if (process.platform === 'win32' && profile.endsWith('.zshrc'))
|
|
202
|
-
continue;
|
|
203
|
-
try {
|
|
204
|
-
if (!fs_1.default.existsSync(profile))
|
|
205
|
-
continue;
|
|
206
|
-
const content = fs_1.default.readFileSync(profile, 'utf8');
|
|
207
|
-
if (content.includes(legacyExportLine)) {
|
|
208
|
-
fs_1.default.writeFileSync(profile, content.replace(legacyExportLine, exportLine), 'utf8');
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
if (content.includes(marker)) {
|
|
212
|
-
if (!content.includes(exportLine)) {
|
|
213
|
-
// Stale export line: versioned dir changed, includePackagedFraimBin
|
|
214
|
-
// toggled, or platform-specific line was never written. Replace it.
|
|
215
|
-
fs_1.default.writeFileSync(profile, content.replace(/export PATH=[\u201D"][^\u201D"]*\.fraim[^\u201D"]*[\u201D"]/, exportLine), 'utf8');
|
|
216
|
-
}
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
fs_1.default.appendFileSync(profile, stanza, 'utf8');
|
|
220
|
-
}
|
|
221
|
-
catch {
|
|
222
|
-
// Ignore unreadable or missing profiles.
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
112
|
function getNextPromptPath() {
|
|
227
113
|
return path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'last-install', 'next-prompt.txt');
|
|
228
114
|
}
|
|
@@ -323,7 +209,7 @@ function buildFirstRunHubLaunchArgs() {
|
|
|
323
209
|
class FirstRunSessionService {
|
|
324
210
|
constructor(options) {
|
|
325
211
|
// Issue #646: the key may be absent when first-run is launched by the macOS
|
|
326
|
-
// installer (.
|
|
212
|
+
// installer (.dmg) instead of `fraim first-run --key=…`. In that case the
|
|
327
213
|
// wizard prompts the user to paste their key (see setKey / needsKey).
|
|
328
214
|
this.key = options.key || '';
|
|
329
215
|
this.headless = options.headless === true;
|
|
@@ -393,13 +279,15 @@ class FirstRunSessionService {
|
|
|
393
279
|
// Real detection — populate row statuses from `command -v` style probes.
|
|
394
280
|
const nodeRow = this.getRow('node');
|
|
395
281
|
const nodeVer = this.embeddedDesktop ? process.version : commandVersion('node');
|
|
396
|
-
|
|
282
|
+
const embeddedLaunchersReady = !this.embeddedDesktop
|
|
283
|
+
|| (fs_1.default.existsSync((0, fraim_mcp_latest_launcher_1.getFraimCliShimPath)()) && fs_1.default.existsSync((0, fraim_mcp_latest_launcher_1.getFraimMcpShimPath)()));
|
|
284
|
+
if (nodeVer && embeddedLaunchersReady) {
|
|
397
285
|
nodeRow.status = 'ok';
|
|
398
286
|
nodeRow.verb = this.embeddedDesktop ? `${nodeVer} bundled with FRAIM` : `${nodeVer} detected`;
|
|
399
287
|
}
|
|
400
288
|
else {
|
|
401
289
|
nodeRow.status = 'pending';
|
|
402
|
-
nodeRow.verb = "we'll install";
|
|
290
|
+
nodeRow.verb = this.embeddedDesktop ? 'installer repair required' : "we'll install";
|
|
403
291
|
}
|
|
404
292
|
const gitRow = this.getRow('git');
|
|
405
293
|
const gitVer = commandVersion('git');
|
|
@@ -424,7 +312,7 @@ class FirstRunSessionService {
|
|
|
424
312
|
fraimRow.status = 'pending';
|
|
425
313
|
fraimRow.verb = "we'll configure your IDEs";
|
|
426
314
|
}
|
|
427
|
-
|
|
315
|
+
(0, persist_shell_path_1.persistShellPath)();
|
|
428
316
|
}
|
|
429
317
|
else {
|
|
430
318
|
fraimRow.status = 'pending';
|
|
@@ -669,17 +557,22 @@ class FirstRunSessionService {
|
|
|
669
557
|
async runNodeRow() {
|
|
670
558
|
const row = this.getRow('node');
|
|
671
559
|
if (this.embeddedDesktop) {
|
|
672
|
-
|
|
560
|
+
if (!this.fakeMode && (!fs_1.default.existsSync((0, fraim_mcp_latest_launcher_1.getFraimCliShimPath)()) || !fs_1.default.existsSync((0, fraim_mcp_latest_launcher_1.getFraimMcpShimPath)()))) {
|
|
561
|
+
this.setRowError(row, 'Checking the native installer setup', 'Stable FRAIM CLI/MCP launchers are missing. Re-run the native installer in repair mode.', [{ id: 'retry', label: 'Check again', variant: 'primary' }]);
|
|
562
|
+
this.persist();
|
|
563
|
+
return this.respond('FRAIM installer repair is required.', false);
|
|
564
|
+
}
|
|
565
|
+
(0, persist_shell_path_1.persistShellPath)();
|
|
673
566
|
row.status = 'ok';
|
|
674
|
-
row.verb =
|
|
567
|
+
row.verb = 'Node and FRAIM launchers ready';
|
|
675
568
|
this.persist();
|
|
676
|
-
return this.respond(
|
|
569
|
+
return this.respond('FRAIM command-line tools are ready.', true);
|
|
677
570
|
}
|
|
678
571
|
// Node is bootstrapped by the install template before the wizard starts.
|
|
679
572
|
// If node is missing here something is severely wrong; fall through to error.
|
|
680
573
|
const ver = commandVersion('node');
|
|
681
574
|
if (ver) {
|
|
682
|
-
|
|
575
|
+
(0, persist_shell_path_1.persistShellPath)();
|
|
683
576
|
row.status = 'ok';
|
|
684
577
|
row.verb = `${ver} installed`;
|
|
685
578
|
this.persist();
|
|
@@ -711,7 +604,7 @@ class FirstRunSessionService {
|
|
|
711
604
|
this.persist();
|
|
712
605
|
return this.respond('git not found — continuing without it. Install git later if you plan to do code delivery work.', true);
|
|
713
606
|
}
|
|
714
|
-
async runFraimRow(
|
|
607
|
+
async runFraimRow() {
|
|
715
608
|
const row = this.getRow('fraim');
|
|
716
609
|
if (this.fakeMode) {
|
|
717
610
|
row.status = 'ok';
|
|
@@ -737,24 +630,12 @@ class FirstRunSessionService {
|
|
|
737
630
|
return this.respond('FRAIM configured.', true);
|
|
738
631
|
}
|
|
739
632
|
try {
|
|
740
|
-
if (this.embeddedDesktop) {
|
|
741
|
-
(
|
|
742
|
-
}
|
|
743
|
-
else if (!forceConfigure && !commandVersion('fraim')) {
|
|
744
|
-
const prefix = path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'node');
|
|
745
|
-
fs_1.default.mkdirSync(prefix, { recursive: true });
|
|
746
|
-
row.streamOutput = 'Installing FRAIM on this machine...';
|
|
747
|
-
this.persist();
|
|
748
|
-
await runProcess('npm', ['install', '-g', 'fraim@latest'], { npm_config_prefix: prefix });
|
|
633
|
+
if (this.embeddedDesktop && (!fs_1.default.existsSync((0, fraim_mcp_latest_launcher_1.getFraimCliShimPath)()) || !fs_1.default.existsSync((0, fraim_mcp_latest_launcher_1.getFraimMcpShimPath)()))) {
|
|
634
|
+
throw new Error('Stable FRAIM launchers are missing. Re-run the native installer in repair mode.');
|
|
749
635
|
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
row.streamOutput = 'Installing FRAIM on this machine...';
|
|
754
|
-
this.persist();
|
|
755
|
-
await runProcess('npm', ['install', '-g', 'fraim@latest'], { npm_config_prefix: prefix });
|
|
756
|
-
}
|
|
757
|
-
persistShellPath(this.embeddedDesktop);
|
|
636
|
+
if (!this.embeddedDesktop)
|
|
637
|
+
(0, fraim_mcp_latest_launcher_1.ensureFraimMcpLatestLauncher)();
|
|
638
|
+
(0, persist_shell_path_1.persistShellPath)();
|
|
758
639
|
const detectedIDEs = (0, ide_detector_1.detectInstalledIDEs)();
|
|
759
640
|
if (detectedIDEs.length > 0) {
|
|
760
641
|
await (0, auto_mcp_setup_1.autoConfigureMCP)(this.key, detectedIDEs.map((ide) => ide.name));
|
|
@@ -1002,7 +883,7 @@ class FirstRunSessionService {
|
|
|
1002
883
|
fraimRow.verb = 'configuring FRAIM for local AI agents';
|
|
1003
884
|
this.persist();
|
|
1004
885
|
appendInstallLog(`agent-recruiting-complete ready=${readyCount}`);
|
|
1005
|
-
return this.runFraimRow(
|
|
886
|
+
return this.runFraimRow();
|
|
1006
887
|
}
|
|
1007
888
|
openTerminalWithCommand(command) {
|
|
1008
889
|
if (process.platform === 'win32') {
|
|
@@ -1051,12 +932,11 @@ class FirstRunSessionService {
|
|
|
1051
932
|
};
|
|
1052
933
|
}
|
|
1053
934
|
if (this.embeddedDesktop) {
|
|
1054
|
-
(0, fraim_mcp_latest_launcher_1.
|
|
1055
|
-
if (!fs_1.default.existsSync((0, fraim_mcp_latest_launcher_1.getFraimCliShimPath)())) {
|
|
935
|
+
if (!fs_1.default.existsSync((0, fraim_mcp_latest_launcher_1.getFraimCliShimPath)()) || !fs_1.default.existsSync((0, fraim_mcp_latest_launcher_1.getFraimMcpShimPath)())) {
|
|
1056
936
|
return {
|
|
1057
937
|
ok: false,
|
|
1058
938
|
needsAgentSetup: true,
|
|
1059
|
-
message: 'FRAIM CLI
|
|
939
|
+
message: 'FRAIM CLI/MCP launchers are missing. Re-run the native installer in repair mode before opening Hub.',
|
|
1060
940
|
};
|
|
1061
941
|
}
|
|
1062
942
|
appendInstallLog('hub-opened embedded-desktop');
|