| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
bc408f5869 |
feat(auth): file-backed and in-memory SecretStore for hosts with no keychain
Closes #1950 Before this, a box with no reachable OS keychain — the published container (no D-Bus session, #1848), Android/Termux (no platform binary, #1905), a minimal Linux install without libsecret — could not persist an OAuth client secret or a stdio `env:` value at all. `KeyringSecretStore` degraded into a store that silently holds nothing: `get` returned null, `delete` no-opped, and `set` hard-failed with a 503. That is the correct failure behavior, but it left the documented, supported way to run the Inspector with no secret persistence whatsoever. Adds the two missing implementations and, more importantly, the policy that picks one and the surfaces that say which was picked. - `FileSecretStore` — one `0600` JSON document at `~/.mcp-inspector/secrets.json`, encrypted with AES-256-GCM whenever `MCP_INSPECTOR_SECRET_KEY` is set (scrypt, per-file random salt). The whole map is encrypted as a unit rather than value-by-value so the account names — `${serverId}:${field}` — are hidden too; a per-value scheme would leave a readable index of which servers you hold a client secret for. A file that can no longer be decrypted reads as empty (matching the interface's read tolerance) but *refuses to be written*, because replacing a file of still-valid secrets to satisfy an additive request destroys data. - `secret-store-selection.ts` — the policy. `MCP_INSPECTOR_SECRET_STORE=keyring|file|memory` wins outright; otherwise probe the keychain and, if unreachable, fall back loudly. The fallback is `memory` in a container whose secrets directory is not on a mount, and `file` otherwise — so mounting the volume the README already recommends for the catalog flips the same run to durable storage with no configuration, and an unmounted container gets the honest answer rather than a file `--rm` will discard. Cached per process so the banner, `/api/config`, and the store doing the writing cannot disagree. - Automatic, but never silent. The downgrade prints at startup, rides `GET /api/config`, and is named in a permanent 32px footer at the bottom of both dialogs that accept a secret. A startup banner is seen once by whoever started the process; a toast is seen once; a dismissible banner is the thing a user dismisses before doing the work it describes. The plaintext and memory cases render their caveat inline in a warning tone — a warning you must hover to discover is not one. - `SecretStoreUnavailableError` is now the base every store's `set` throws, so the routes translate one condition into a 503 instead of matching `KeychainUnavailableError` by name. Docs: the container section of the root README gains a table of which store you get and how to make secrets survive, plus the `MCP_INSPECTOR_SECRET_KEY` upgrade/mismatch rules. Tests: the existing interface contract block is extracted to `secretStoreContract.ts` and run against all three implementations, so none can drift on the availability contract. `FileSecretStore` is tested against a real temp filesystem — mode, cross-instance round-trip, that the ciphertext does not contain the plaintext, the read-modify-write race, and every refusal path. Full `npm run ci` passes, per-file gate included. Signed-off-by: cliffhall <cliff@futurescale.com> |
||
|
|
8aba3122c5 |
refactor(web): consolidate the three GET /api/config hooks into one (#1770)
Some checks failed
Run install, format, lint, build, and test on every push / build (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish-github-container-registry (push) Has been cancelled
* refactor(web): consolidate the three GET /api/config hooks into one (#1643) useSandboxUrl, useServerListWritable, and useInspectorVersion each fetched the same static /api/config payload separately and read one field from it — three requests on load for one payload. Replace them with a single useInitialConfig hook that fetches once and exposes { version, sandboxUrl, writable, loading } with the same tolerant parsing/defaults (writable defaults true; version/sandboxUrl undefined when absent; unmount-safe). - Add core/react/useInitialConfig.ts; wire App.tsx to it. - Remove the three former single-field hooks and their tests. - Fold the fetch-branch matrix into one useInitialConfig test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * test(web): address review nits on useInitialConfig tests (#1643) - "keeps writable true for any non-false value" now sends real falsy-but- not-false values a nonconforming backend could actually emit (null / 0 / "no") instead of `writable: undefined`, which was indistinguishable from an absent key and duplicated the omitted-field test. - Soften the post-unmount comment to say the test exercises the isCancelled() guard branches for coverage rather than implying it can detect their removal (React 18 dropped the setState-after-unmount warning). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * docs(web): fix contradicting unmount-guard comment; it.each writable cases (#1643) Second-review follow-ups: - core/react/useInitialConfig.ts: correct the isCancelled() rationale — React 18 no longer warns on setState-after-unmount; the guard exists to stop a stale payload (after unmount or a baseUrl/authToken re-run) overwriting current state. The old wording contradicted the corrected test comment. - useInitialConfig.test.tsx: convert the writable-non-false loop to it.each so each value (null / 0 / "no") reports independently. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * test(web): use vitest %j specifier in it.each title; tidy hook docblock (#1643) Third-review follow-ups (both title/comment-only): - useInitialConfig.test.tsx: `%p` is a Jest specifier vitest doesn't support, so all three cases rendered the literal `%p`. Switch to `%j` — titles now render null / 0 / "no" distinctly (verified: `writable=null`, `writable=0`, `writable="no"`). - useInitialConfig.ts: extend the class docblock to say a response is dropped after unmount OR a re-fetch, matching the corrected inline guard comment. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc408f5869 | ||
|
|
8aba3122c5 |