fix(auth): keep the failure path total, and align the docs with the new order (#2144)

Copilot review round 14. Two code fixes and a documentation sweep.

- `buildRevocationRequest` ran outside `revokeToken`'s best-effort try.
  `encodeURIComponent` throws on a lone UTF-16 surrogate — valid JSON, so it
  can reach there from a persisted client id or secret — and every caller has
  already cleared its local state by then, so a rejection instead of a `failed`
  outcome would break the documented guarantee. Construction is inside the try
  now, and the catch reports `params.endpoint` rather than a `url` that may not
  exist on that path.
- The fixture's Basic decode turned a malformed percent escape into an Express
  500, reporting a bad credential as a server fault rather than as the
  `invalid_client` 401 the endpoint means. Decoding failure is failed
  authentication.
- Ten places still said revocation happens *before* the clear, which the
  reorder in the previous commit made false: the settings-form description, the
  `--no-revoke` help text, both READMEs, `docs/mcp-server-configuration.md`,
  three JSDoc blocks, and the PR body. Swept, and the ordering is stated where
  it is load-bearing rather than asserted in passing. Also merged the two
  adjacent JSDoc blocks on `InspectorClient.clearOAuthTokens`.

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 05:55:00 -04:00
commit 4429c0c0bf

View file

@ -282,6 +282,27 @@ describe("revokeToken", () => {
});
});
// A lone UTF-16 surrogate is valid JSON, so it can reach here from a
// persisted client id or secret and makes `encodeURIComponent` throw. Every
// caller has already cleared its local state by the time this runs, so a
// rejection here would break the documented best-effort guarantee.
it("reports an unencodable credential as failed rather than throwing", async () => {
const outcome = await revokeToken({
endpoint: REVOKE_URL,
token: "r",
tokenTypeHint: "refresh_token",
clientInformation: {
client_id: "cid",
// Lone high surrogate.
client_secret: `bad${String.fromCharCode(0xd800)}`,
},
supportedAuthMethods: ["client_secret_basic"],
fetchFn: vi.fn<typeof fetch>(),
});
expect(outcome).toMatchObject({ status: "failed", endpoint: REVOKE_URL });
});
it("reports a network failure as failed", async () => {
const fetchFn = vi.fn<typeof fetch>(async () => {
throw new Error("connect ECONNREFUSED");