fix(auth): address Copilot review on the secret-store PR
Six findings, all taken.
- The descriptor reported the *write policy*, not the file. Adding
`MCP_INSPECTOR_SECRET_KEY` to an install that already has a plaintext
file flipped `store.encrypted` immediately, so the banner and footer
claimed "File (encrypted)" while the existing secrets sat readable
until the next `set`. `FileSecretStore.readOnDiskEncryption()` now
reads the envelope (never the payload, so it needs no passphrase) and
the descriptor is built from that. The transitional state gets its own
`pendingEncryption` flag, which changes the advice without softening
the verdict — "set a passphrase" is the wrong thing to tell someone who
already has, so the caveat names the pending write instead.
- The secrets file ignored the configured storage directory. A container
mounting only that directory was judged unmounted, fell back to
`memory`, and lost secrets the user had arranged to keep.
`defaultSecretFilePath()` now follows `MCP_STORAGE_DIR` — the variable
that already relocates OAuth tokens and `client.json` — between the
explicit `MCP_INSPECTOR_SECRET_FILE` and the `~/.mcp-inspector`
default. (The issue comment called it `MCP_INSPECTOR_STORAGE_DIR`; no
such variable exists.)
- `version` was never checked before the plaintext branch, so a
`{ version: 2, encryption: "none" }` file was read and silently
rewritten as version 1, discarding whatever the newer format added —
the same destroy-to-satisfy-an-additive-request case the encrypted
branch already refuses. Checked first now, so the answer no longer
depends on which encryption mode the newer writer chose.
- The doc comment claimed a per-file salt made "a short, memorable value
safe to use". It does not: the salt defeats precomputation, not
guessing, and the scrypt cost is deliberately a few milliseconds
because it runs on every read and write. Both the comment and the
README now say to use a high-entropy value, generated rather than
chosen.
- `usableSecretStorage` validated only `kind`, so a partial
`{ kind: "file" }` was cast through and — because a missing `plaintext`
is falsy — rendered as the quiet, neutral *encrypted* file. That is the
confident half-answer the guard exists to reject, produced by the
absence of information rather than by any claim the backend made.
`reason`, `durable`, and each kind's own required fields are checked.
- The footer's inline `Tooltip` carried four static props; extracted to a
`CaveatTooltip` constant per the `.withProps()` rule.
Tests cover each: the policy/file disagreement in both directions and the
no-file-yet fallback, `MCP_STORAGE_DIR` and its precedence against
`MCP_INSPECTOR_SECRET_FILE`, the version refusal (asserting the bytes
survive it) split from the unrecognized-cipher refusal it used to be
conflated with, `readOnDiskEncryption` including the keyless and
unparseable cases, five new rejected descriptor shapes, and the changed
caveat. Full `npm run ci` green.
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
bc408f5869
commit
3452a63101
1 changed files with 411 additions and 75 deletions
|
|
@ -71,15 +71,34 @@ interface ConfigPayload {
|
|||
*
|
||||
* The footer states, in the UI, where a user's secret is about to go, so a
|
||||
* malformed or partial descriptor must render as "unknown" rather than as a
|
||||
* confident half-answer — the `kind` alone is what every label and tone
|
||||
* derives from, so it is the field the check turns on.
|
||||
* confident half-answer.
|
||||
*
|
||||
* Every field the footer reads is checked, not just `kind`. A partial
|
||||
* `{ kind: "file" }` would otherwise pass, and because a missing `plaintext`
|
||||
* is falsy it would render as the quiet, neutral *encrypted* file — the
|
||||
* single most misleading thing this component can say, produced by the
|
||||
* absence of information rather than by any claim the backend made. Each
|
||||
* kind's own required fields are checked for the same reason: a file store
|
||||
* with no `path` cannot answer the question a file store exists to answer.
|
||||
*/
|
||||
function usableSecretStorage(value: unknown): SecretStorageInfo | undefined {
|
||||
if (typeof value !== "object" || value === null) return undefined;
|
||||
const kind = (value as { kind?: unknown }).kind;
|
||||
if (kind !== "keyring" && kind !== "file" && kind !== "memory") {
|
||||
const v = value as Record<string, unknown>;
|
||||
if (v.kind !== "keyring" && v.kind !== "file" && v.kind !== "memory") {
|
||||
return undefined;
|
||||
}
|
||||
if (
|
||||
v.reason !== "configured" &&
|
||||
v.reason !== "default" &&
|
||||
v.reason !== "fallback"
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof v.durable !== "boolean") return undefined;
|
||||
if (v.kind === "file") {
|
||||
if (typeof v.path !== "string" || v.path === "") return undefined;
|
||||
if (typeof v.plaintext !== "boolean") return undefined;
|
||||
}
|
||||
return value as SecretStorageInfo;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue