fix(auth): address Copilot review round 16
Three findings. The first is the third consecutive round in which my previous fix introduced the next defect, all in the same ~15 lines of `useInitialConfig`. **The request token did not subsume the unmount guard, and I said it did.** Bumping the generation on effect teardown invalidates loads already *in flight*; a `refresh()` called after unmount claims the next token, is therefore current, and commits into a dead hook. That is a live path — the callers fire `refresh()` from an async settings persist that can outlive `App`. Mounted state is tracked separately again and `stale()` checks both. The test for this was also wrong, in the way that matters: it called `refresh()` after unmount but never settled the fetch, so it could not observe the commit it existed to prevent. It settles the promise now. **A tmpfs mount was reported as durable.** `docker run --tmpfs /home/node/.mcp-inspector` puts a real entry in mountinfo, so a mount-table lookup answered "durable", selected the file store, and published `durable: true` for a file that disappears when the container stops — the exact false promise the in-memory fallback exists to avoid, reached by trusting the mount table over the filesystem type. `tmpfs`/`ramfs` are excluded now, read from the field after the ` - ` separator rather than by position, since the optional fields before it are variable in number. **A malformed envelope was diagnosed as a changed passphrase.** A truncated payload, a wrong-length IV or tag, or an out-of-range KDF parameter all threw `SecretFileKeyMismatchError`, whose 503 tells the user to restore a passphrase that cannot repair a structurally broken file. The read path now runs the same `encryptedEnvelopeProblem` check the descriptor path already ran, and reserves the key-mismatch error for an envelope that is well-formed and fails authentication — which is the case where that advice is right. Two existing tests asserted the old behavior and encoded the bug; both updated, and a new one pins that a well-formed envelope with the wrong key still gets the passphrase advice. 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
0ab542cbee
commit
52087f7739
1 changed files with 160 additions and 18 deletions
|
|
@ -68,9 +68,11 @@ export interface UseInitialConfigResult {
|
|||
* the descriptor per request; this is the client half of that.
|
||||
*
|
||||
* Safe to call at any time and safe to overlap: the GET is idempotent, and
|
||||
* loads are ordered by a request token, so only the most recent one may
|
||||
* commit — a response that arrives after unmount, after a re-fetch, or out
|
||||
* of order behind a newer refresh is dropped.
|
||||
* loads are ordered by a request token *and* gated on the hook still being
|
||||
* mounted — so a response arriving after unmount, after a re-fetch, or out
|
||||
* of order behind a newer refresh is dropped. Calling it after unmount is a
|
||||
* no-op rather than a state update, which matters because the callers fire
|
||||
* it from an async persist that can outlive the tree.
|
||||
*/
|
||||
refresh: () => void;
|
||||
}
|
||||
|
|
@ -193,14 +195,28 @@ export function useInitialConfig(
|
|||
* matters more here than in most places precisely because the field it
|
||||
* would revert is a security statement.
|
||||
*
|
||||
* Bumping on effect teardown covers the other two cases for free: unmount,
|
||||
* and a `baseUrl`/`authToken` change that re-runs the effect.
|
||||
* Bumping on effect teardown also covers a `baseUrl`/`authToken` change
|
||||
* that re-runs the effect, by invalidating whatever that run started.
|
||||
*
|
||||
* It does **not** cover unmount on its own, and an earlier version of this
|
||||
* comment claimed it did. The teardown bump only invalidates loads already
|
||||
* in flight; a `refresh()` called *after* unmount — which happens when an
|
||||
* async settings persist resolves after `App` has gone — claims the next
|
||||
* token, is therefore current, and commits into a dead hook. Mounted state
|
||||
* is tracked separately for that, and `stale()` checks both.
|
||||
*/
|
||||
const generation = useRef(0);
|
||||
const mounted = useRef(true);
|
||||
useEffect(() => {
|
||||
mounted.current = true;
|
||||
return () => {
|
||||
mounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const load = useCallback(async (): Promise<void> => {
|
||||
const mine = ++generation.current;
|
||||
const stale = () => generation.current !== mine;
|
||||
const stale = () => !mounted.current || generation.current !== mine;
|
||||
const headers: Record<string, string> = {};
|
||||
if (authToken) headers["x-mcp-remote-auth"] = `Bearer ${authToken}`;
|
||||
try {
|
||||
|
|
|
|||
Loading…
Reference in a new issue