fix(auth): address Copilot review round 2 on the RFC 7009 revocation (#2144)

- `revokeStoredKeys`' pre-read of `getTokens` sat outside the best-effort
  catch. `getTokens` parses through `OAuthTokensSchema`, so a persisted token
  that no longer validates rejected and abandoned the local delete `--relogin`
  promises — over a grant that could not have been revoked anyway. Caught and
  retained as a failed outcome; the clear proceeds.
- The multi-key bookkeeping was wrong twice. A token is now marked spent only
  on a `revoked` outcome, so a failed or unsupported attempt no longer skips a
  duplicate entry under the other key that may hold the credentials or metadata
  that would have worked. And a failure now outranks an earlier success for
  reporting, so a stale grant left live cannot be hidden behind the first key's
  200.
- `--no-revoke` is now rejected without `--relogin`, ahead of the short-circuit
  returns, matching the parser's existing stance on accepted-but-inert flags
  (`--strict`). On its own it reads as "this run will not revoke anything",
  true only because nothing was being cleared.
- Extracted the App draft merge as `utils/serverWithDraftSettings`, so the rule
  has a test. The hook tests receive whatever they are handed and cannot tell a
  draft from a persisted entry, and `App.test.tsx`'s settings harness connects
  a stdio server where the OAuth section never renders.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0177mPHAdECD18nLLCTwR5rh
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-28 01:47:48 -04:00
commit 4a24c4880d

View file

@ -185,6 +185,20 @@ describe("--relogin token revocation", () => {
}
});
// Inert-but-accepted flags are rejected in this parser (see the `--strict`
// rationale in `cli.ts`). On its own `--no-revoke` reads as "this run will
// not revoke anything", which is true only because nothing was being cleared.
it("rejects --no-revoke without --relogin", async () => {
const result = await runCli([
"--no-revoke",
"--server-url",
SERVER_URL,
"--method",
"tools/list",
]);
expect(result.stderr).toMatch(/--no-revoke requires --relogin/);
});
// A failed revocation must be visible — the grant is still live at the
// authorization server — without turning `--relogin` into a failure, which
// is a local delete the user still gets.