fraim 2.0.327 → 2.0.329
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/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 +4 -2
- package/dist/src/cli/mcp/fraim-mcp-latest-launcher.js +176 -7
- package/dist/src/cli/utils/managed-agent-install.js +5 -1
- 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 +9 -135
- 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/session-service.js +2 -1
- package/package.json +1 -1
|
@@ -11,6 +11,7 @@ exports.resolveSubscriptionNextBillingDateWithInvoiceFallback = resolveSubscript
|
|
|
11
11
|
exports.buildCurrentPeriodEndUpdate = buildCurrentPeriodEndUpdate;
|
|
12
12
|
exports.resolveInvoicePaidNextBillingDate = resolveInvoicePaidNextBillingDate;
|
|
13
13
|
const stripe_1 = require("../../config/stripe");
|
|
14
|
+
const redact_secrets_1 = require("../../core/utils/redact-secrets");
|
|
14
15
|
const feature_flags_1 = require("../../config/feature-flags");
|
|
15
16
|
const email_service_1 = require("../../services/email-service");
|
|
16
17
|
const dashboard_access_1 = require("../../services/dashboard-access");
|
|
@@ -273,7 +274,7 @@ async function handleSubscriptionCreatedOrUpdated(subscription, dbService) {
|
|
|
273
274
|
expiresAt: null, // Subscriptions don't have fixed expiration
|
|
274
275
|
cancelAt: subscription.cancel_at ? new Date(subscription.cancel_at * 1000) : null
|
|
275
276
|
});
|
|
276
|
-
console.log(`✅ Upgraded trial key to paid subscription: ${apiKey.key}`);
|
|
277
|
+
console.log(`✅ Upgraded trial key to paid subscription: ${(0, redact_secrets_1.redactApiKey)(apiKey.key)}`);
|
|
277
278
|
}
|
|
278
279
|
else {
|
|
279
280
|
// Create new API key for subscription (shouldn't happen in normal flow)
|
|
@@ -295,7 +296,7 @@ async function handleSubscriptionCreatedOrUpdated(subscription, dbService) {
|
|
|
295
296
|
apiCallCount: 0,
|
|
296
297
|
personaSystemActive: (0, feature_flags_1.isPersonaEntitlementsEnabled)() || undefined
|
|
297
298
|
});
|
|
298
|
-
console.log(`✅ Created new API key for subscription: ${newKey}`);
|
|
299
|
+
console.log(`✅ Created new API key for subscription: ${(0, redact_secrets_1.redactApiKey)(newKey)}`);
|
|
299
300
|
apiKey = { key: newKey, userId: email };
|
|
300
301
|
}
|
|
301
302
|
}
|
|
@@ -358,7 +359,7 @@ async function handleSubscriptionDeleted(subscription, dbService) {
|
|
|
358
359
|
// Send cancellation email
|
|
359
360
|
const emailService = new email_service_1.EmailService();
|
|
360
361
|
await emailService.sendSubscriptionCancelled(apiKey.userId, new Date());
|
|
361
|
-
console.log(`✅ API key expired due to subscription cancellation: ${apiKey.key}`);
|
|
362
|
+
console.log(`✅ API key expired due to subscription cancellation: ${(0, redact_secrets_1.redactApiKey)(apiKey.key)}`);
|
|
362
363
|
}
|
|
363
364
|
async function handleInvoicePaid(invoice, dbService) {
|
|
364
365
|
console.log(`✅ Invoice paid: ${invoice.id}`);
|
|
@@ -438,7 +439,7 @@ async function handleInvoicePaid(invoice, dbService) {
|
|
|
438
439
|
suspendedAt: null,
|
|
439
440
|
suspensionReason: null
|
|
440
441
|
});
|
|
441
|
-
console.log(`✅ API key restored after payment: ${apiKey.key}`);
|
|
442
|
+
console.log(`✅ API key restored after payment: ${(0, redact_secrets_1.redactApiKey)(apiKey.key)}`);
|
|
442
443
|
// Send restoration email
|
|
443
444
|
const emailService = new email_service_1.EmailService();
|
|
444
445
|
const restorationBillingDate = nextBillingDate || new Date();
|
|
@@ -467,7 +468,7 @@ async function handleInvoicePaid(invoice, dbService) {
|
|
|
467
468
|
}
|
|
468
469
|
// Regular renewal
|
|
469
470
|
{
|
|
470
|
-
console.log(`✅ Subscription renewed: ${apiKey.key}`);
|
|
471
|
+
console.log(`✅ Subscription renewed: ${(0, redact_secrets_1.redactApiKey)(apiKey.key)}`);
|
|
471
472
|
if (nextBillingDate) {
|
|
472
473
|
await dbService.updateApiKey(apiKey.key, buildCurrentPeriodEndUpdate(nextBillingDate));
|
|
473
474
|
}
|
|
@@ -532,7 +533,7 @@ async function handleInvoicePaymentFailed(invoice, dbService) {
|
|
|
532
533
|
suspendedAt: new Date(),
|
|
533
534
|
suspensionReason: 'payment_failed'
|
|
534
535
|
});
|
|
535
|
-
console.log(`✅ API key suspended due to payment failure: ${apiKey.key}`);
|
|
536
|
+
console.log(`✅ API key suspended due to payment failure: ${(0, redact_secrets_1.redactApiKey)(apiKey.key)}`);
|
|
536
537
|
// Send payment failure email with 7-day grace period
|
|
537
538
|
const emailService = new email_service_1.EmailService();
|
|
538
539
|
const gracePeriodEndsAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
|
|
@@ -376,9 +376,18 @@ const runSync = async (options) => {
|
|
|
376
376
|
await refreshOrgCache(remoteUrl, apiKey);
|
|
377
377
|
// 1d. Refresh manager home
|
|
378
378
|
await refreshManagerCache(remoteUrl, apiKey);
|
|
379
|
-
// 1e. Refresh MCP proxy launcher
|
|
380
|
-
|
|
379
|
+
// 1e. Refresh MCP proxy launcher, and record the version this sync is running as the one
|
|
380
|
+
// the launcher should use next. Issue #1354: the launcher reuses its recorded version inside
|
|
381
|
+
// a freshness window instead of resolving npm on every launch, so without this a published
|
|
382
|
+
// fix reaches an installed machine up to a window late and nothing the user can run shortens
|
|
383
|
+
// that. Writing the record here makes `npx fraim@latest sync` the deliberate way to pull a
|
|
384
|
+
// release through immediately: it resolves latest, runs as latest, and records latest, which
|
|
385
|
+
// the next MCP server spawn picks up at once.
|
|
386
|
+
const { ensureFraimMcpLatestLauncher, recordFraimMcpLauncherVersion } = await Promise.resolve().then(() => __importStar(require('../mcp/fraim-mcp-latest-launcher')));
|
|
381
387
|
ensureFraimMcpLatestLauncher();
|
|
388
|
+
if (recordFraimMcpLauncherVersion((0, version_utils_1.getFraimVersion)())) {
|
|
389
|
+
console.log(chalk_1.default.green(` MCP launcher will use fraim@${(0, version_utils_1.getFraimVersion)()} on its next launch`));
|
|
390
|
+
}
|
|
382
391
|
// 1f. Ensure user-level directories and runtime dependencies (Issue #1508:
|
|
383
392
|
// synced scripts like the DOCX review layer require npm deps such as
|
|
384
393
|
// adm-zip; a plain sync must install them, not just create directories)
|
|
@@ -12,15 +12,14 @@ exports.buildAgentDebugSteps = buildAgentDebugSteps;
|
|
|
12
12
|
exports.interpretationForClassification = interpretationForClassification;
|
|
13
13
|
exports.analyzeFraimProxyLog = analyzeFraimProxyLog;
|
|
14
14
|
exports.diagnoseFraimServerConfigConsistency = diagnoseFraimServerConfigConsistency;
|
|
15
|
-
const crypto_1 = __importDefault(require("crypto"));
|
|
16
15
|
const fs_1 = __importDefault(require("fs"));
|
|
16
|
+
const redact_secrets_1 = require("../../../core/utils/redact-secrets");
|
|
17
17
|
exports.DEFAULT_FRAIM_REMOTE_URL = 'https://fraim.wellnessatwork.me';
|
|
18
18
|
exports.FRAIM_PROXY_LOG_FILE = 'fraim-mcp-proxy.log';
|
|
19
19
|
function fingerprintSecret(secret) {
|
|
20
20
|
return {
|
|
21
|
-
|
|
22
|
-
length: secret.length
|
|
23
|
-
sha256: crypto_1.default.createHash('sha256').update(secret).digest('hex').slice(0, 16)
|
|
21
|
+
ref: (0, redact_secrets_1.redactApiKey)(secret),
|
|
22
|
+
length: secret.length
|
|
24
23
|
};
|
|
25
24
|
}
|
|
26
25
|
function normalizeUrl(url) {
|
|
@@ -33,9 +32,11 @@ function appendLimited(current, chunk, maxChars = 8000) {
|
|
|
33
32
|
function sanitizeDiagnosticText(text, knownSecrets = []) {
|
|
34
33
|
let sanitized = text;
|
|
35
34
|
for (const secret of knownSecrets.filter(Boolean)) {
|
|
36
|
-
sanitized = sanitized.split(secret).join(
|
|
35
|
+
sanitized = sanitized.split(secret).join((0, redact_secrets_1.redactApiKey)(secret));
|
|
37
36
|
}
|
|
38
|
-
|
|
37
|
+
// Issue #1701: a truncated key is still disclosed key material. Substitute the
|
|
38
|
+
// same reference the rest of FRAIM logs so diagnostics stay correlatable.
|
|
39
|
+
return sanitized.replace(/fraim_[A-Za-z0-9_-]+/g, (match) => (0, redact_secrets_1.redactApiKey)(match));
|
|
39
40
|
}
|
|
40
41
|
function readFileTailUtf8(filePath, maxBytes = 256 * 1024) {
|
|
41
42
|
const stat = fs_1.default.statSync(filePath);
|
|
@@ -133,7 +134,13 @@ function interpretationForClassification(classification) {
|
|
|
133
134
|
return 'The proxy log did not match a known FRAIM MCP startup pattern.';
|
|
134
135
|
}
|
|
135
136
|
}
|
|
136
|
-
function analyzeFraimProxyLog(logContent,
|
|
137
|
+
function analyzeFraimProxyLog(logContent,
|
|
138
|
+
/**
|
|
139
|
+
* `expectedKeyRef` is the redacted reference the proxy writes in its per-line
|
|
140
|
+
* `[...]` segment (issue #1701), not a raw key. Callers holding a raw key must
|
|
141
|
+
* pass `redactApiKey(key)`.
|
|
142
|
+
*/
|
|
143
|
+
options = {}) {
|
|
137
144
|
const maxLines = options.maxLines ?? 300;
|
|
138
145
|
const allLines = logContent.split(/\r?\n/).map(line => line.trim()).filter(Boolean).slice(-maxLines);
|
|
139
146
|
if (allLines.length === 0) {
|
|
@@ -152,10 +159,10 @@ function analyzeFraimProxyLog(logContent, options = {}) {
|
|
|
152
159
|
};
|
|
153
160
|
}
|
|
154
161
|
const keyLineRegex = /^\S+\s+\[([^\]]+)\]\s+(.*)$/;
|
|
155
|
-
const matchingLines = options.
|
|
156
|
-
? allLines.filter(line => keyLineRegex.exec(line)?.[1] === options.
|
|
162
|
+
const matchingLines = options.expectedKeyRef
|
|
163
|
+
? allLines.filter(line => keyLineRegex.exec(line)?.[1] === options.expectedKeyRef)
|
|
157
164
|
: allLines;
|
|
158
|
-
if (options.
|
|
165
|
+
if (options.expectedKeyRef && matchingLines.length === 0) {
|
|
159
166
|
const classification = 'no-log-lines-for-expected-key';
|
|
160
167
|
return {
|
|
161
168
|
classification,
|
|
@@ -55,6 +55,7 @@ const child_process_1 = require("child_process");
|
|
|
55
55
|
const win32_cmd_spawn_1 = require("../../utils/win32-cmd-spawn");
|
|
56
56
|
const axios_1 = __importDefault(require("axios"));
|
|
57
57
|
const toml = __importStar(require("toml"));
|
|
58
|
+
const redact_secrets_1 = require("../../../core/utils/redact-secrets");
|
|
58
59
|
const ide_detector_1 = require("../../setup/ide-detector");
|
|
59
60
|
const fraim_mcp_latest_launcher_1 = require("../../mcp/fraim-mcp-latest-launcher");
|
|
60
61
|
const fraim_mcp_diagnostics_1 = require("./fraim-mcp-diagnostics");
|
|
@@ -302,7 +303,7 @@ async function testFraimConnectivity() {
|
|
|
302
303
|
if (process.env.DEBUG_DOCTOR) {
|
|
303
304
|
console.log('[DOCTOR DEBUG] Testing FRAIM connectivity:');
|
|
304
305
|
console.log(' URL:', remoteUrl);
|
|
305
|
-
console.log(' API Key:',
|
|
306
|
+
console.log(' API Key:', (0, redact_secrets_1.redactApiKey)(apiKey));
|
|
306
307
|
console.log(' NODE_ENV:', process.env.NODE_ENV || 'not set');
|
|
307
308
|
}
|
|
308
309
|
// Warn if testing against localhost (might be in test mode)
|
|
@@ -631,7 +632,8 @@ async function testFraimLocalProxyTroubleshooting() {
|
|
|
631
632
|
}
|
|
632
633
|
const logTail = (0, fraim_mcp_diagnostics_1.readFileTailUtf8)(logPath);
|
|
633
634
|
const analysis = (0, fraim_mcp_diagnostics_1.analyzeFraimProxyLog)(logTail.content, {
|
|
634
|
-
|
|
635
|
+
// The proxy tags each line with the key's reference, never the key (issue #1701).
|
|
636
|
+
expectedKeyRef: (0, redact_secrets_1.redactApiKey)(keyDiagnostic.apiKey),
|
|
635
637
|
maxLines: 300
|
|
636
638
|
});
|
|
637
639
|
const errorClassifications = [
|
|
@@ -8,12 +8,58 @@ exports.getFraimMcpShimPath = getFraimMcpShimPath;
|
|
|
8
8
|
exports.getFraimNpxShimPath = getFraimNpxShimPath;
|
|
9
9
|
exports.getFraimCliShimPath = getFraimCliShimPath;
|
|
10
10
|
exports.getPackagedFraimRuntimePath = getPackagedFraimRuntimePath;
|
|
11
|
+
exports.getFraimMcpLauncherStatePath = getFraimMcpLauncherStatePath;
|
|
12
|
+
exports.recordFraimMcpLauncherVersion = recordFraimMcpLauncherVersion;
|
|
11
13
|
exports.ensureFraimMcpLatestLauncher = ensureFraimMcpLatestLauncher;
|
|
12
14
|
const fs_1 = __importDefault(require("fs"));
|
|
13
15
|
const os_1 = __importDefault(require("os"));
|
|
14
16
|
const path_1 = __importDefault(require("path"));
|
|
15
17
|
const windows_shim_path_search_1 = require("../../core/utils/windows-shim-path-search");
|
|
16
|
-
const LAUNCHER_VERSION =
|
|
18
|
+
const LAUNCHER_VERSION = 2;
|
|
19
|
+
/**
|
|
20
|
+
* Issue #1354: how long a resolved version stays good before the launcher asks npm again.
|
|
21
|
+
*
|
|
22
|
+
* Resolving "latest" is a registry round trip by definition, so a launcher that resolves on
|
|
23
|
+
* every invocation pays that cost on every invocation - and pays a full connection timeout
|
|
24
|
+
* when the registry is unreachable, which is the silent-startup-failure this issue reports.
|
|
25
|
+
* Bounding it to one resolve per window is what makes the offline path cheap: with a fresh
|
|
26
|
+
* record there is no reason to touch the network at all.
|
|
27
|
+
*
|
|
28
|
+
* The tradeoff is explicit: inside the window the launcher runs the last version it resolved,
|
|
29
|
+
* so "always latest" is really "latest as of at most one window ago". 24h is not a new number -
|
|
30
|
+
* it matches `FraimLocalMCPServer.DEFAULT_CONNECT_SYNC_MAX_AGE_MS`, the connect-time catalog
|
|
31
|
+
* freshness window, rather than introducing a second unrelated one.
|
|
32
|
+
*/
|
|
33
|
+
const STALENESS_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
34
|
+
/**
|
|
35
|
+
* How long the resolve may spend talking to the registry before giving up.
|
|
36
|
+
*
|
|
37
|
+
* Paired with `--fetch-retries=0`: the resolve is always optional, because a failure falls back
|
|
38
|
+
* to the recorded version, so the only thing a long wait buys is the difference between a brief
|
|
39
|
+
* pause and an apparent hang at process spawn. 5s is comfortably above a healthy registry round
|
|
40
|
+
* trip (measured at roughly 0.7-1.4s on this machine) and well inside an MCP client's startup
|
|
41
|
+
* window, which is the budget this issue reports blowing.
|
|
42
|
+
*/
|
|
43
|
+
const RESOLVE_TIMEOUT_MS = 5000;
|
|
44
|
+
/** Per-version npm caches to keep. Older ones are pruned so the directory stays bounded. */
|
|
45
|
+
const CACHE_GENERATIONS_KEPT = 2;
|
|
46
|
+
/**
|
|
47
|
+
* A per-version cache is only ever a prune candidate once it is this old.
|
|
48
|
+
*
|
|
49
|
+
* The generation count alone is not a safe rule here, because these directories are not inert
|
|
50
|
+
* caches: `runPackage` points `npm_config_cache` at one, and npx installs the package *inside*
|
|
51
|
+
* it at `_npx/<hash>/node_modules`. An MCP stdio server lives as long as its agent session and
|
|
52
|
+
* resolves dynamic imports from that tree for its whole life, so removing the cache of a version
|
|
53
|
+
* still being served would pull the module tree out from under a running process. On Windows it
|
|
54
|
+
* is worse than a clean failure: `rmSync` deletes whatever is not currently open and throws on
|
|
55
|
+
* the rest, leaving a half-gutted tree.
|
|
56
|
+
*
|
|
57
|
+
* An age floor is the cheap correct guard, and matches how this repository already bounds
|
|
58
|
+
* on-disk growth elsewhere (`artifact-retention-cleanup` retires by age in days, not by count).
|
|
59
|
+
* Growth stays bounded - everything past the newest few ages out - while any cache a plausibly
|
|
60
|
+
* live session could still be reading is off limits.
|
|
61
|
+
*/
|
|
62
|
+
const CACHE_MIN_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
17
63
|
const launcherSource = `#!/usr/bin/env node
|
|
18
64
|
const fs = require('fs');
|
|
19
65
|
const os = require('os');
|
|
@@ -21,6 +67,10 @@ const path = require('path');
|
|
|
21
67
|
const { spawnSync } = require('child_process');
|
|
22
68
|
|
|
23
69
|
const LAUNCHER_VERSION = ${LAUNCHER_VERSION};
|
|
70
|
+
const STALENESS_WINDOW_MS = ${STALENESS_WINDOW_MS};
|
|
71
|
+
const RESOLVE_TIMEOUT_MS = ${RESOLVE_TIMEOUT_MS};
|
|
72
|
+
const CACHE_GENERATIONS_KEPT = ${CACHE_GENERATIONS_KEPT};
|
|
73
|
+
const CACHE_MIN_AGE_MS = ${CACHE_MIN_AGE_MS};
|
|
24
74
|
const USER_DIR = process.env.FRAIM_USER_DIR || path.join(os.homedir(), '.fraim');
|
|
25
75
|
const STATE_DIR = path.join(USER_DIR, 'mcp-launcher');
|
|
26
76
|
const STATE_PATH = path.join(STATE_DIR, 'latest.json');
|
|
@@ -64,10 +114,14 @@ function writeState(state) {
|
|
|
64
114
|
}
|
|
65
115
|
|
|
66
116
|
function resolveLatest(packageName) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
117
|
+
// Fail fast rather than waiting out npm's default retry ladder. This resolve is always
|
|
118
|
+
// optional: a failure falls back to the recorded version, so a long wait buys nothing and is
|
|
119
|
+
// the difference between a brief pause and an apparent hang when the registry is unreachable.
|
|
120
|
+
const result = runCommand(
|
|
121
|
+
npmCommand,
|
|
122
|
+
['view', packageName, 'version', '--silent', '--fetch-retries=0', '--fetch-timeout=' + RESOLVE_TIMEOUT_MS],
|
|
123
|
+
{ encoding: 'utf8', env: process.env }
|
|
124
|
+
);
|
|
71
125
|
|
|
72
126
|
if (result.status !== 0) {
|
|
73
127
|
return null;
|
|
@@ -77,7 +131,47 @@ function resolveLatest(packageName) {
|
|
|
77
131
|
return /^\\d+\\.\\d+\\.\\d+/.test(version) ? version : null;
|
|
78
132
|
}
|
|
79
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Whether a record names a version this launcher can run.
|
|
136
|
+
*
|
|
137
|
+
* Deliberately not gated on launcherVersion. The record's shape is what matters and that has
|
|
138
|
+
* not changed, so a record written by an earlier launcher is still a usable fallback. Rejecting
|
|
139
|
+
* one would mean the first launch after a launcher upgrade fails outright when the registry is
|
|
140
|
+
* unreachable, turning a working offline start into an error for no benefit.
|
|
141
|
+
*/
|
|
142
|
+
function isUsableState(state) {
|
|
143
|
+
// The version shape is re-checked here, not only where npm answers. This record is the common
|
|
144
|
+
// path now rather than a rare fallback, and its value reaches both an npx --package argument
|
|
145
|
+
// and a cache directory name, so it is validated at the point of use rather than trusted
|
|
146
|
+
// because writeState happened to produce it.
|
|
147
|
+
return Boolean(
|
|
148
|
+
state &&
|
|
149
|
+
state.packageName === 'fraim' &&
|
|
150
|
+
state.binName === 'fraim' &&
|
|
151
|
+
typeof state.version === 'string' &&
|
|
152
|
+
/^\\d+\\.\\d+\\.\\d+/.test(state.version)
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** True while the recorded resolve is recent enough to reuse without asking npm. */
|
|
157
|
+
function isFresh(state) {
|
|
158
|
+
if (!isUsableState(state)) return false;
|
|
159
|
+
const recordedAt = Date.parse(state.updatedAt);
|
|
160
|
+
if (!Number.isFinite(recordedAt)) return false;
|
|
161
|
+
const age = Date.now() - recordedAt;
|
|
162
|
+
// A record stamped in the future is a clock change, not freshness.
|
|
163
|
+
return age >= 0 && age < STALENESS_WINDOW_MS;
|
|
164
|
+
}
|
|
165
|
+
|
|
80
166
|
function resolvePackage() {
|
|
167
|
+
// Read the freshness record before reaching for the network. This ordering is the fix: the
|
|
168
|
+
// record used to be consulted only after the resolve had already failed, so the happy path
|
|
169
|
+
// was gated on npm reachability on every single launch.
|
|
170
|
+
const cached = readState();
|
|
171
|
+
if (isFresh(cached)) {
|
|
172
|
+
return { packageName: 'fraim', binName: 'fraim', version: cached.version, fromCache: true };
|
|
173
|
+
}
|
|
174
|
+
|
|
81
175
|
const fraimVersion = resolveLatest('fraim');
|
|
82
176
|
if (fraimVersion) {
|
|
83
177
|
const state = { packageName: 'fraim', binName: 'fraim', version: fraimVersion, fromCache: false };
|
|
@@ -85,8 +179,7 @@ function resolvePackage() {
|
|
|
85
179
|
return state;
|
|
86
180
|
}
|
|
87
181
|
|
|
88
|
-
|
|
89
|
-
if (cached && cached.packageName === 'fraim' && cached.binName === 'fraim' && cached.version) {
|
|
182
|
+
if (isUsableState(cached)) {
|
|
90
183
|
console.error('[fraim-mcp-launcher] Could not resolve npm latest; using cached fraim@' + cached.version + '.');
|
|
91
184
|
return { packageName: 'fraim', binName: 'fraim', version: cached.version, fromCache: true };
|
|
92
185
|
}
|
|
@@ -95,6 +188,37 @@ function resolvePackage() {
|
|
|
95
188
|
process.exit(1);
|
|
96
189
|
}
|
|
97
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Keep the newest few per-version caches and age out the rest. Each entry is a full npm cache
|
|
193
|
+
* for one version (~80MB), plus one retry-<timestamp>-<pid> directory per Windows launch
|
|
194
|
+
* failure, so without a bound the directory grows once per release forever.
|
|
195
|
+
*
|
|
196
|
+
* A directory is removed only when it is BOTH outside the newest CACHE_GENERATIONS_KEPT and
|
|
197
|
+
* older than CACHE_MIN_AGE_MS. The age floor is what keeps this safe: the package is installed
|
|
198
|
+
* inside the cache directory, so a recent one may still be backing a running MCP server.
|
|
199
|
+
*
|
|
200
|
+
* Best effort throughout: a cache that cannot be removed is not a reason to fail the launch.
|
|
201
|
+
*/
|
|
202
|
+
function pruneCaches(keepDir, nowMs) {
|
|
203
|
+
try {
|
|
204
|
+
const entries = fs.readdirSync(CACHE_ROOT, { withFileTypes: true })
|
|
205
|
+
.filter(function (entry) { return entry.isDirectory(); })
|
|
206
|
+
.map(function (entry) {
|
|
207
|
+
const full = path.join(CACHE_ROOT, entry.name);
|
|
208
|
+
let mtime = 0;
|
|
209
|
+
try { mtime = fs.statSync(full).mtimeMs; } catch (error) { mtime = 0; }
|
|
210
|
+
return { full: full, mtime: mtime };
|
|
211
|
+
})
|
|
212
|
+
.filter(function (entry) { return entry.full !== keepDir; })
|
|
213
|
+
.sort(function (a, b) { return b.mtime - a.mtime; });
|
|
214
|
+
|
|
215
|
+
for (const entry of entries.slice(Math.max(0, CACHE_GENERATIONS_KEPT - 1))) {
|
|
216
|
+
if (nowMs - entry.mtime < CACHE_MIN_AGE_MS) continue;
|
|
217
|
+
try { fs.rmSync(entry.full, { recursive: true, force: true }); } catch (error) { /* leave it */ }
|
|
218
|
+
}
|
|
219
|
+
} catch (error) { /* nothing to prune */ }
|
|
220
|
+
}
|
|
221
|
+
|
|
98
222
|
function runPackage(plan, cacheDir) {
|
|
99
223
|
ensureDir(cacheDir);
|
|
100
224
|
const env = { ...process.env, npm_config_cache: cacheDir };
|
|
@@ -107,6 +231,7 @@ function runPackage(plan, cacheDir) {
|
|
|
107
231
|
|
|
108
232
|
const plan = resolvePackage();
|
|
109
233
|
const versionCacheDir = path.join(CACHE_ROOT, plan.packageName + '-' + plan.version.replace(/[^a-zA-Z0-9._-]/g, '_'));
|
|
234
|
+
pruneCaches(versionCacheDir, Date.now());
|
|
110
235
|
let status = runPackage(plan, versionCacheDir);
|
|
111
236
|
|
|
112
237
|
if (status !== 0 && process.platform === 'win32') {
|
|
@@ -132,6 +257,50 @@ function getFraimCliShimPath() {
|
|
|
132
257
|
function getPackagedFraimRuntimePath() {
|
|
133
258
|
return path_1.default.join(process.env.FRAIM_USER_DIR || path_1.default.join(os_1.default.homedir(), '.fraim'), 'bin', 'packaged-fraim-runtime.json');
|
|
134
259
|
}
|
|
260
|
+
function getFraimMcpLauncherStatePath() {
|
|
261
|
+
return path_1.default.join(process.env.FRAIM_USER_DIR || path_1.default.join(os_1.default.homedir(), '.fraim'), 'mcp-launcher', 'latest.json');
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Record `version` as the version the launcher should run, resetting the freshness window.
|
|
265
|
+
*
|
|
266
|
+
* This is the deliberate escape hatch from `STALENESS_WINDOW_MS`. The window is what keeps
|
|
267
|
+
* ordinary launches off the network, but it also means a published fix reaches an installed
|
|
268
|
+
* machine up to a window late. `fraim sync` calls this with the version it is itself running, so
|
|
269
|
+
* `npx fraim@latest sync` resolves latest, runs as latest, and records latest - which the next
|
|
270
|
+
* MCP server spawn then uses immediately instead of waiting out the window.
|
|
271
|
+
*
|
|
272
|
+
* The semantics are "the version you just ran is the version recorded", which is why the caller
|
|
273
|
+
* passes its own version rather than this function consulting the registry. A sync that ran an
|
|
274
|
+
* older package must not claim to have recorded a newer one.
|
|
275
|
+
*
|
|
276
|
+
* The record's shape is duplicated from the generated launcher's `writeState`, deliberately and
|
|
277
|
+
* unavoidably: that script executes standalone from `~/.fraim/bin` with only Node built-ins and
|
|
278
|
+
* cannot import this module. The two writers must stay in step, and
|
|
279
|
+
* `test-1354-mcp-launcher-freshness-window.ts` asserts that a record written here is accepted by
|
|
280
|
+
* the launcher rather than merely that the fields look alike.
|
|
281
|
+
*/
|
|
282
|
+
function recordFraimMcpLauncherVersion(version) {
|
|
283
|
+
if (!/^\d+\.\d+\.\d+/.test(version))
|
|
284
|
+
return false;
|
|
285
|
+
const statePath = getFraimMcpLauncherStatePath();
|
|
286
|
+
try {
|
|
287
|
+
fs_1.default.mkdirSync(path_1.default.dirname(statePath), { recursive: true });
|
|
288
|
+
fs_1.default.writeFileSync(statePath, JSON.stringify({
|
|
289
|
+
packageName: 'fraim',
|
|
290
|
+
binName: 'fraim',
|
|
291
|
+
version,
|
|
292
|
+
fromCache: false,
|
|
293
|
+
launcherVersion: LAUNCHER_VERSION,
|
|
294
|
+
updatedAt: new Date().toISOString()
|
|
295
|
+
}, null, 2));
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
// Best effort. A sync that cannot write this record has still synced the catalog, and the
|
|
300
|
+
// launcher resolves normally when the window expires; failing the sync would be worse.
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
135
304
|
const windowsFraimMcpShimSource = `@echo off
|
|
136
305
|
setlocal
|
|
137
306
|
for %%I in ("%~dp0..") do set "FRAIM_USER_DIR_VALUE=%%~fI"
|
|
@@ -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
|
}
|
|
@@ -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
|
}
|
|
@@ -426,7 +312,7 @@ class FirstRunSessionService {
|
|
|
426
312
|
fraimRow.status = 'pending';
|
|
427
313
|
fraimRow.verb = "we'll configure your IDEs";
|
|
428
314
|
}
|
|
429
|
-
|
|
315
|
+
(0, persist_shell_path_1.persistShellPath)();
|
|
430
316
|
}
|
|
431
317
|
else {
|
|
432
318
|
fraimRow.status = 'pending';
|
|
@@ -676,7 +562,7 @@ class FirstRunSessionService {
|
|
|
676
562
|
this.persist();
|
|
677
563
|
return this.respond('FRAIM installer repair is required.', false);
|
|
678
564
|
}
|
|
679
|
-
|
|
565
|
+
(0, persist_shell_path_1.persistShellPath)();
|
|
680
566
|
row.status = 'ok';
|
|
681
567
|
row.verb = 'Node and FRAIM launchers ready';
|
|
682
568
|
this.persist();
|
|
@@ -686,7 +572,7 @@ class FirstRunSessionService {
|
|
|
686
572
|
// If node is missing here something is severely wrong; fall through to error.
|
|
687
573
|
const ver = commandVersion('node');
|
|
688
574
|
if (ver) {
|
|
689
|
-
|
|
575
|
+
(0, persist_shell_path_1.persistShellPath)();
|
|
690
576
|
row.status = 'ok';
|
|
691
577
|
row.verb = `${ver} installed`;
|
|
692
578
|
this.persist();
|
|
@@ -718,7 +604,7 @@ class FirstRunSessionService {
|
|
|
718
604
|
this.persist();
|
|
719
605
|
return this.respond('git not found — continuing without it. Install git later if you plan to do code delivery work.', true);
|
|
720
606
|
}
|
|
721
|
-
async runFraimRow(
|
|
607
|
+
async runFraimRow() {
|
|
722
608
|
const row = this.getRow('fraim');
|
|
723
609
|
if (this.fakeMode) {
|
|
724
610
|
row.status = 'ok';
|
|
@@ -747,21 +633,9 @@ class FirstRunSessionService {
|
|
|
747
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)()))) {
|
|
748
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
|
-
else if (forceConfigure && !commandVersion('fraim')) {
|
|
758
|
-
const prefix = path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'node');
|
|
759
|
-
fs_1.default.mkdirSync(prefix, { recursive: true });
|
|
760
|
-
row.streamOutput = 'Installing FRAIM on this machine...';
|
|
761
|
-
this.persist();
|
|
762
|
-
await runProcess('npm', ['install', '-g', 'fraim@latest'], { npm_config_prefix: prefix });
|
|
763
|
-
}
|
|
764
|
-
persistShellPath(this.embeddedDesktop);
|
|
636
|
+
if (!this.embeddedDesktop)
|
|
637
|
+
(0, fraim_mcp_latest_launcher_1.ensureFraimMcpLatestLauncher)();
|
|
638
|
+
(0, persist_shell_path_1.persistShellPath)();
|
|
765
639
|
const detectedIDEs = (0, ide_detector_1.detectInstalledIDEs)();
|
|
766
640
|
if (detectedIDEs.length > 0) {
|
|
767
641
|
await (0, auto_mcp_setup_1.autoConfigureMCP)(this.key, detectedIDEs.map((ide) => ide.name));
|
|
@@ -1009,7 +883,7 @@ class FirstRunSessionService {
|
|
|
1009
883
|
fraimRow.verb = 'configuring FRAIM for local AI agents';
|
|
1010
884
|
this.persist();
|
|
1011
885
|
appendInstallLog(`agent-recruiting-complete ready=${readyCount}`);
|
|
1012
|
-
return this.runFraimRow(
|
|
886
|
+
return this.runFraimRow();
|
|
1013
887
|
}
|
|
1014
888
|
openTerminalWithCommand(command) {
|
|
1015
889
|
if (process.platform === 'win32') {
|
|
@@ -62,6 +62,7 @@ const crypto_1 = require("crypto");
|
|
|
62
62
|
const axios_1 = __importDefault(require("axios"));
|
|
63
63
|
const provider_utils_1 = require("../core/utils/provider-utils");
|
|
64
64
|
const object_utils_1 = require("../core/utils/object-utils");
|
|
65
|
+
const redact_secrets_1 = require("../core/utils/redact-secrets");
|
|
65
66
|
const pack_home_1 = require("../cli/utils/pack-home");
|
|
66
67
|
const local_registry_resolver_1 = require("../core/utils/local-registry-resolver");
|
|
67
68
|
const ai_mentor_1 = require("../core/ai-mentor");
|
|
@@ -553,7 +554,7 @@ class FraimLocalMCPServer {
|
|
|
553
554
|
}
|
|
554
555
|
this.log('🚀 FRAIM Local MCP Server starting... [DEBUG-PROXY-V3]');
|
|
555
556
|
this.log(`📡 Remote server: ${this.remoteUrl}`);
|
|
556
|
-
this.log(`🔑 API key: ${
|
|
557
|
+
this.log(`🔑 API key: ${(0, redact_secrets_1.redactApiKey)(this.apiKey)}`);
|
|
557
558
|
this.log(`Local MCP version: ${this.localVersion}`);
|
|
558
559
|
this.log(`🔍 DEBUG BUILD: Machine detection v2 active`);
|
|
559
560
|
// Initialize usage collector
|
|
@@ -626,23 +627,35 @@ class FraimLocalMCPServer {
|
|
|
626
627
|
this.log(`Could not persist userEmail: ${e instanceof Error ? e.message : String(e)}`);
|
|
627
628
|
}
|
|
628
629
|
}
|
|
630
|
+
/**
|
|
631
|
+
* Non-reversible reference for this proxy's key (issue #1701). Every line the
|
|
632
|
+
* proxy writes carries it, and the proxy mirrors those lines into a temp file
|
|
633
|
+
* that survives the process, so the key itself must never appear here. The
|
|
634
|
+
* reference matches the one the server logs for the same key, so a user's local
|
|
635
|
+
* log still correlates with the server-side log.
|
|
636
|
+
*/
|
|
637
|
+
keyRef() {
|
|
638
|
+
return this.apiKey ? (0, redact_secrets_1.redactApiKey)(this.apiKey) : 'MISSING_API_KEY';
|
|
639
|
+
}
|
|
629
640
|
log(message) {
|
|
630
641
|
// Log to stderr (stdout is reserved for MCP protocol)
|
|
631
|
-
const
|
|
632
|
-
|
|
642
|
+
const keyRef = this.keyRef();
|
|
643
|
+
// Messages are assembled from remote responses and caught errors, either of
|
|
644
|
+
// which can carry a credential this proxy did not author.
|
|
645
|
+
const safeMessage = (0, redact_secrets_1.redactSecretsInText)(message);
|
|
646
|
+
console.error(`[FRAIM key:${keyRef}] ${safeMessage}`);
|
|
633
647
|
// Also log to file for debugging
|
|
634
648
|
try {
|
|
635
649
|
const fs = require('fs');
|
|
636
650
|
const logFile = require('path').join(require('os').tmpdir(), 'fraim-mcp-proxy.log');
|
|
637
|
-
fs.appendFileSync(logFile, `${new Date().toISOString()} [${
|
|
651
|
+
fs.appendFileSync(logFile, `${new Date().toISOString()} [${keyRef}] ${safeMessage}\n`);
|
|
638
652
|
}
|
|
639
653
|
catch (e) {
|
|
640
654
|
// Ignore file logging errors
|
|
641
655
|
}
|
|
642
656
|
}
|
|
643
657
|
logError(message) {
|
|
644
|
-
|
|
645
|
-
console.error(`[FRAIM ERROR key:${key}] ${message}`);
|
|
658
|
+
console.error(`[FRAIM ERROR key:${this.keyRef()}] ${(0, redact_secrets_1.redactSecretsInText)(message)}`);
|
|
646
659
|
}
|
|
647
660
|
detectLocalVersion() {
|
|
648
661
|
const candidates = [
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.AuthMiddleware = void 0;
|
|
4
|
+
const redact_secrets_1 = require("../core/utils/redact-secrets");
|
|
4
5
|
const fs_1 = require("fs");
|
|
5
6
|
const path_1 = require("path");
|
|
6
7
|
const cookie_service_1 = require("../services/cookie-service");
|
|
@@ -219,7 +220,7 @@ class AuthMiddleware {
|
|
|
219
220
|
try {
|
|
220
221
|
const apiKeyData = await this.dbService.verifyApiKey(apiKey);
|
|
221
222
|
if (!apiKeyData) {
|
|
222
|
-
console.error(`❌ FRAIM AUTH: Invalid API key: ${apiKey}`);
|
|
223
|
+
console.error(`❌ FRAIM AUTH: Invalid API key: ${(0, redact_secrets_1.redactApiKey)(apiKey)}`);
|
|
223
224
|
setMcpAuthenticateChallenge();
|
|
224
225
|
return res.status(401).json({
|
|
225
226
|
jsonrpc: '2.0',
|
|
@@ -230,7 +231,7 @@ class AuthMiddleware {
|
|
|
230
231
|
// Check if key is suspended
|
|
231
232
|
if (apiKeyData.status === 'suspended') {
|
|
232
233
|
const reason = apiKeyData.suspensionReason || 'payment failure';
|
|
233
|
-
console.error(`❌ FRAIM AUTH: Suspended API key: ${apiKey} (reason: ${reason})`);
|
|
234
|
+
console.error(`❌ FRAIM AUTH: Suspended API key: ${(0, redact_secrets_1.redactApiKey)(apiKey)} (reason: ${reason})`);
|
|
234
235
|
return res.status(402).json({
|
|
235
236
|
jsonrpc: '2.0',
|
|
236
237
|
error: {
|
|
@@ -255,7 +256,7 @@ class AuthMiddleware {
|
|
|
255
256
|
const message = tier === 'trial'
|
|
256
257
|
? 'Your 14-day free trial has expired. Please upgrade to continue using FRAIM.'
|
|
257
258
|
: 'Your subscription has expired. Please renew to continue using FRAIM.';
|
|
258
|
-
console.error(`❌ FRAIM AUTH: Expired API key: ${apiKey} (tier: ${tier})`);
|
|
259
|
+
console.error(`❌ FRAIM AUTH: Expired API key: ${(0, redact_secrets_1.redactApiKey)(apiKey)} (tier: ${tier})`);
|
|
259
260
|
return res.status(402).json({
|
|
260
261
|
jsonrpc: '2.0',
|
|
261
262
|
error: {
|
|
@@ -35,38 +35,21 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.requestLogger = requestLogger;
|
|
37
37
|
const crypto_1 = require("crypto");
|
|
38
|
+
const redact_secrets_1 = require("../core/utils/redact-secrets");
|
|
38
39
|
const fs = __importStar(require("fs"));
|
|
39
40
|
const path = __importStar(require("path"));
|
|
40
41
|
const LOG_FILE = path.join(process.cwd(), 'server-direct.log');
|
|
41
|
-
/**
|
|
42
|
-
* Extracts API key from request for logging purposes
|
|
43
|
-
*/
|
|
44
|
-
function getApiKeyForLogging(req) {
|
|
45
|
-
const fromContext = req.apiKeyData?.key;
|
|
46
|
-
if (typeof fromContext === 'string' && fromContext.trim().length > 0) {
|
|
47
|
-
return fromContext;
|
|
48
|
-
}
|
|
49
|
-
const fromHeader = Array.isArray(req.headers['x-api-key'])
|
|
50
|
-
? req.headers['x-api-key'][0]
|
|
51
|
-
: req.headers['x-api-key'];
|
|
52
|
-
if (typeof fromHeader === 'string' && fromHeader.trim().length > 0) {
|
|
53
|
-
return fromHeader;
|
|
54
|
-
}
|
|
55
|
-
const fromQuery = req.query?.['api-key'];
|
|
56
|
-
if (typeof fromQuery === 'string' && fromQuery.trim().length > 0) {
|
|
57
|
-
return fromQuery;
|
|
58
|
-
}
|
|
59
|
-
return 'ANONYMOUS';
|
|
60
|
-
}
|
|
61
42
|
function getBodyForLogging(req) {
|
|
62
43
|
if (req.path === '/api/ai-hub/brand' && req.body && typeof req.body === 'object') {
|
|
63
44
|
const body = { ...req.body };
|
|
64
45
|
if (typeof body.logo === 'string' && body.logo.length > 0) {
|
|
65
46
|
body.logo = `[redacted logo payload, ${body.logo.length} chars]`;
|
|
66
47
|
}
|
|
67
|
-
return JSON.stringify(body);
|
|
48
|
+
return (0, redact_secrets_1.redactSecretsInText)(JSON.stringify(body));
|
|
68
49
|
}
|
|
69
|
-
|
|
50
|
+
// Bodies are logged verbatim; a caller that puts a credential in one must not
|
|
51
|
+
// have it land in the log in the clear (issue #1701).
|
|
52
|
+
return (0, redact_secrets_1.redactSecretsInText)(JSON.stringify(req.body));
|
|
70
53
|
}
|
|
71
54
|
/**
|
|
72
55
|
* Middleware to log all requests
|
|
@@ -118,8 +101,8 @@ function requestLogger(req, res, next) {
|
|
|
118
101
|
res.on('finish', () => {
|
|
119
102
|
const duration = Date.now() - start;
|
|
120
103
|
const status = res.statusCode;
|
|
121
|
-
const
|
|
122
|
-
const logMsg = `[req:${requestId}] [local:${localMcpVersion}] ${
|
|
104
|
+
const keyRef = (0, redact_secrets_1.apiKeyRefFromRequest)(req);
|
|
105
|
+
const logMsg = `[req:${requestId}] [local:${localMcpVersion}] ${keyRef} END ${method} ${path} ${status} ${duration}ms`;
|
|
123
106
|
logEntry(logMsg);
|
|
124
107
|
});
|
|
125
108
|
next();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TelemetryMiddleware = void 0;
|
|
4
|
+
const redact_secrets_1 = require("../core/utils/redact-secrets");
|
|
4
5
|
class TelemetryMiddleware {
|
|
5
6
|
constructor(sessionManager) {
|
|
6
7
|
this.sessionManager = sessionManager;
|
|
@@ -125,7 +126,7 @@ After successful fraim_connect, all other FRAIM tools will work. This is require
|
|
|
125
126
|
id: req.body?.id || null
|
|
126
127
|
});
|
|
127
128
|
}
|
|
128
|
-
console.log(`Telemetry active for ${apiKey}`);
|
|
129
|
+
console.log(`Telemetry active for ${(0, redact_secrets_1.redactApiKey)(apiKey)}`);
|
|
129
130
|
return next();
|
|
130
131
|
}
|
|
131
132
|
catch (error) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SessionManager = void 0;
|
|
4
|
+
const redact_secrets_1 = require("../core/utils/redact-secrets");
|
|
4
5
|
class SessionManager {
|
|
5
6
|
constructor(dbService) {
|
|
6
7
|
this.sessions = new Map();
|
|
@@ -42,7 +43,7 @@ class SessionManager {
|
|
|
42
43
|
: await this.dbService.getActiveSessionByApiKey(apiKey);
|
|
43
44
|
if (dbSession) {
|
|
44
45
|
sessionKey = this.getSessionKey(apiKey, dbSession.sessionId);
|
|
45
|
-
console.log(`Re-hydrating session for API Key: ${apiKey}, sessionId: ${dbSession.sessionId}`);
|
|
46
|
+
console.log(`Re-hydrating session for API Key: ${(0, redact_secrets_1.redactApiKey)(apiKey)}, sessionId: ${dbSession.sessionId}`);
|
|
46
47
|
session = {
|
|
47
48
|
sessionId: dbSession.sessionId,
|
|
48
49
|
lastWrite: now,
|