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
|
|
@ -216,14 +216,40 @@ function unescapeMountPoint(point: string): string {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filesystem types that live in RAM and vanish with the container.
|
||||
*
|
||||
* `docker run --tmpfs /home/node/.mcp-inspector` produces a real entry in
|
||||
* mountinfo, so a mount-table lookup alone answers "durable" and selects the
|
||||
* file store — publishing `durable: true` for a file that disappears when the
|
||||
* container stops. That is the same false promise the in-memory fallback
|
||||
* exists to avoid, reached by believing the mount table over the filesystem
|
||||
* type.
|
||||
*/
|
||||
const EPHEMERAL_FSTYPES = new Set(["tmpfs", "ramfs"]);
|
||||
|
||||
/**
|
||||
* Durable mount points visible to this process, or `null` where mountinfo
|
||||
* cannot be read.
|
||||
*
|
||||
* Field 5 is the mount point; the filesystem type is the first field *after*
|
||||
* the optional-fields separator (` - `), which is why the line is split on
|
||||
* that rather than indexed positionally — the optional fields before it are
|
||||
* variable in number, so `fields[8]` is not reliable.
|
||||
*/
|
||||
function readMountPoints(): string[] | null {
|
||||
try {
|
||||
return fsSync
|
||||
.readFileSync("/proc/self/mountinfo", "utf-8")
|
||||
.split("\n")
|
||||
.map((line) => line.split(" ")[4])
|
||||
.filter((point): point is string => Boolean(point))
|
||||
.map(unescapeMountPoint)
|
||||
.flatMap((line) => {
|
||||
const point = line.split(" ")[4];
|
||||
if (!point) return [];
|
||||
const after = line.split(" - ")[1];
|
||||
const fstype = after?.trim().split(/\s+/)[0];
|
||||
if (fstype && EPHEMERAL_FSTYPES.has(fstype)) return [];
|
||||
return [unescapeMountPoint(point)];
|
||||
})
|
||||
.filter((point) => point !== "/");
|
||||
} catch {
|
||||
return null;
|
||||
|
|
|
|||
Loading…
Reference in a new issue