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
1 changed files with 101 additions and 20 deletions
|
|
@ -111,6 +111,7 @@ import { useMessageLog } from "@inspector/core/react/useMessageLog.js";
|
|||
import { useFetchRequestLog } from "@inspector/core/react/useFetchRequestLog.js";
|
||||
import { useStderrLog } from "@inspector/core/react/useStderrLog.js";
|
||||
import { useInitialConfig } from "@inspector/core/react/useInitialConfig.js";
|
||||
import { refreshingPersist } from "./utils/refreshingPersist";
|
||||
import { usePendingClientRequests } from "@inspector/core/react/usePendingClientRequests.js";
|
||||
import { InspectorView } from "./components/views/InspectorView/InspectorView";
|
||||
import type {
|
||||
|
|
@ -3995,16 +3996,14 @@ function App() {
|
|||
targetId: settingsModalTargetId,
|
||||
resolveInitial: (id) =>
|
||||
servers.find((s) => s.id === id)?.settings ?? EMPTY_SETTINGS,
|
||||
onPersist: async (id: string, settings: InspectorServerSettings) => {
|
||||
await updateServerSettings(id, settings);
|
||||
// The saved settings may include an OAuth client secret or a stdio
|
||||
// `env:` value, and writing one can change what the secrets file *is*
|
||||
// — the first save under a newly-set passphrase re-encrypts a
|
||||
// pre-existing plaintext file. The backend re-derives the descriptor
|
||||
// per request, so ask again; otherwise the footer keeps reporting the
|
||||
// state from page load for the rest of the session (#1950 review r14).
|
||||
refreshInitialConfig();
|
||||
},
|
||||
// The saved settings may include an OAuth client secret or a stdio `env:`
|
||||
// value, and writing one can change what the secrets file *is* — the first
|
||||
// save under a newly-set passphrase re-encrypts a pre-existing plaintext
|
||||
// file. `refreshingPersist` re-asks the backend afterwards; it is a named
|
||||
// unit rather than an inline `await …; refresh()` because this file is
|
||||
// outside the coverage gate, so wiring written here is tested by nothing
|
||||
// (#1950 review r17).
|
||||
onPersist: refreshingPersist(updateServerSettings, refreshInitialConfig),
|
||||
// Surface failures via toast — the modal usually closes
|
||||
// immediately on user dismiss, so a silent fail-on-flush would
|
||||
// leave the user thinking their last edits saved when they
|
||||
|
|
@ -4028,7 +4027,10 @@ function App() {
|
|||
} = useClientSettingsDraft({
|
||||
opened: clientSettingsOpen,
|
||||
resolveInitial: () => clientConfigToFormValues(clientConfig),
|
||||
onPersist: async (values) => {
|
||||
// Wrapped for the same reason as the server-settings persist above: this
|
||||
// one can carry the enterprise IdP client secret, and that write is what
|
||||
// flips a pending-encryption file to encrypted.
|
||||
onPersist: refreshingPersist(async (values) => {
|
||||
if (!canPersistClientSettingsDraft(values)) return;
|
||||
const next = formValuesToClientConfig(values);
|
||||
await saveClientConfigRemote(next, {
|
||||
|
|
@ -4036,11 +4038,7 @@ function App() {
|
|||
authToken: getAuthToken(),
|
||||
});
|
||||
setClientConfig(next);
|
||||
// Same reason as the server-settings persist: this can carry the
|
||||
// enterprise IdP client secret, and that write is what flips a
|
||||
// pending-encryption file to encrypted.
|
||||
refreshInitialConfig();
|
||||
},
|
||||
}, refreshInitialConfig),
|
||||
onError: (err) => {
|
||||
notifications.show({
|
||||
title: "Failed to save client settings",
|
||||
|
|
|
|||
Loading…
Reference in a new issue