fix(auth): address Copilot review round 17
Three findings. The first is the fourth consecutive one in the refresh path, so it is answered by making that path testable rather than by patching it again. **The refresh-after-persist wiring was covered by nothing.** It lived as two inline `await …; refresh()` pairs in `App.tsx`, which is deliberately outside the coverage `include` — so the hook's tests and the modals' tests could both pass while the callback joining them was broken, leaving the security footer stale after exactly the write that changed it. Three of the last four rounds found defects in this path; none of them could have been caught by a test, because there was nowhere for one to live. Extracted to `utils/refreshingPersist`, which is inside the gate and has its own tests: refresh runs after the persist resolves (not before — refreshing first re-reads the descriptor the pending write is about to invalidate), arguments pass through unchanged (the server-settings caller is `(id, settings)`, and a transparent wrapper is the difference between saving the entry and dropping it), and a failed persist refreshes nothing while propagating the error. **A tmpfs-style false promise, in the path fallback.** With neither `HOME` nor `USERPROFILE` set — an ordinary service environment — `defaultSecretFilePath` returned `.mcp-inspector/secrets.json`, a *relative* path, while `FileSecretStoreOptions.filePath` and the `SecretStorageInfo.path` the footer offers to copy both promise an absolute one. Resolved like the two override branches above it. **A warning that was usually false.** The session-store notice fired on every `/api/servers` read whenever the store was in-memory — including for the default empty catalog, where "plaintext values are left on disk" is not true of anything, repeated on every list refresh. It is now emitted lazily, at most once per server instance, and only when the loop actually preserves a non-empty secrets set. A log line that is usually untrue is one people learn to skip, which costs it the occasion it matters. The remaining suppressed comment is the hand-off's check-to-delete race, tracked in #2082. Full `npm run ci` green. Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
52087f7739
commit
78a65f7c20
2 changed files with 101 additions and 20 deletions
|
|
@ -117,7 +117,13 @@ export function defaultSecretFilePath(): string {
|
|||
const storageDir = process.env[STORAGE_DIR_ENV]?.trim();
|
||||
if (storageDir) return path.resolve(storageDir, "secrets.json");
|
||||
const homeDir = process.env.HOME || process.env.USERPROFILE || ".";
|
||||
return path.join(homeDir, ".mcp-inspector", "secrets.json");
|
||||
// Resolved like the two override branches. A service started with a minimal
|
||||
// environment has neither variable, and the `"."` fallback then yields
|
||||
// `.mcp-inspector/secrets.json` — a *relative* path, while
|
||||
// `FileSecretStoreOptions.filePath` and the `SecretStorageInfo.path` the UI
|
||||
// copies both promise an absolute one. The footer would offer to copy a
|
||||
// location that means nothing without knowing the process's cwd.
|
||||
return path.resolve(homeDir, ".mcp-inspector", "secrets.json");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1940,17 +1940,28 @@ export function createRemoteApp(
|
|||
// it and abandon the migration for this GET — the on-disk file stays
|
||||
// as-is so the user's secret isn't lost, and the next GET retries
|
||||
// (e.g. after the user installs libsecret).
|
||||
// Warned lazily, at most once per server instance, and only when there is
|
||||
// actually something being preserved. Announcing it up front meant every
|
||||
// `/api/servers` read on a session-scoped store logged that plaintext
|
||||
// values were left on disk — including for the default empty catalog,
|
||||
// where the statement is simply false, repeated on every list refresh.
|
||||
// Hoisted out of the function so it is once per process rather than once
|
||||
// per read.
|
||||
let warnedSessionPreserved = false;
|
||||
|
||||
const migratePlaintextSecrets = async (
|
||||
config: MCPConfig,
|
||||
): Promise<{ migrated: MCPConfig; changed: boolean }> => {
|
||||
let changed = false;
|
||||
const next: MCPConfig = { mcpServers: {} };
|
||||
const durable = await secretStoreIsDurable(secretStore);
|
||||
if (!durable && fileLogger) {
|
||||
const warnSessionPreserved = () => {
|
||||
if (warnedSessionPreserved || durable || !fileLogger) return;
|
||||
warnedSessionPreserved = true;
|
||||
fileLogger.warn(
|
||||
"Secrets are kept in memory for this session, so plaintext values in mcp.json are left on disk rather than migrated away. They would otherwise be lost when the Inspector exits.",
|
||||
);
|
||||
}
|
||||
};
|
||||
try {
|
||||
for (const [id, stored] of Object.entries(config.mcpServers)) {
|
||||
const { stripped, secrets } = extractSecretsFromStored(stored);
|
||||
|
|
@ -1982,6 +1993,7 @@ export function createRemoteApp(
|
|||
// loaded into the store above, so this session behaves normally;
|
||||
// only the disk delete is withheld.
|
||||
if (!durable) {
|
||||
warnSessionPreserved();
|
||||
next.mcpServers[id] = stored;
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue