fix(auth): salvage a malformed fallback registration, and correct the stale API names (#2144)

Copilot review round 19.

- Parsing the preconfigured client registration sat outside the per-slot
  salvage, so one malformed fallback aborted the whole plan and grants carrying
  their own valid credentials were never revoked. It is a *fallback*; a bad one
  is now recorded and skipped, matching every other slot.
- `sendPlans` preserved an earlier success when a later key was never
  attempted, contradicting the failure-first rule two lines below — the
  unattempted key's grant may still be live. That branch turns out to be a
  bound rather than a path (`withDeadline` fails any plan that reaches the
  deadline, so a preceding plan cannot both succeed and leave zero budget), so
  it carries a justified `v8 ignore` saying exactly that.
- `clearStoredAuthForRelogin` takes an optional `budgetMs`, which is what lets
  the shared-budget behavior be tested without a five-second test.
- Three stale references to `planOAuthRevocation`, which no longer exists —
  two JSDoc links and the `AGENTS.md` entry, which additionally still told
  callers to make a separate `storage.clear()` call. Following that text would
  have reintroduced the second clear the atomic step exists to remove.

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 07:57:45 -04:00
commit 579a3eeeb9

View file

@ -438,7 +438,7 @@ interface CachedMetadata {
}
/**
* Send the requests a {@link planOAuthRevocation} snapshot describes.
* Send the requests a {@link clearAndPlanRevocation} snapshot describes.
*
* Safe to run **after** `storage.clear(serverUrl)` — that is the point: the
* plan already holds the tokens, the credentials and the endpoint, so nothing
@ -539,9 +539,20 @@ async function collectGrants(
const failures: TokenRevocationOutcome[] = [];
const seenKeys = new Set<string>();
const preregistered = await parseClient(
snapshot.preregisteredClientInformation,
);
// Parsed defensively: it is only a *fallback*, so a malformed preconfigured
// registration must not abort grants that carry their own valid credentials.
// Recorded and skipped, matching the per-slot salvage below.
let preregistered: OAuthClientInformation | undefined;
try {
preregistered = await parseClient(snapshot.preregisteredClientInformation);
} catch (err) {
failures.push({
status: "failed",
detail: `could not read the preconfigured client registration: ${
err instanceof Error ? err.message : String(err)
}`,
});
}
const add = async (
issuer: string | undefined,
@ -643,7 +654,7 @@ async function runPlan(
): Promise<TokenRevocationOutcome> {
if (plan.outcome) return plan.outcome;
const endpoint = plan.endpoint;
/* v8 ignore next -- `planOAuthRevocation` always sets `outcome` when it sets
/* v8 ignore next -- `clearAndPlanRevocation` always sets `outcome` when it sets
no endpoint, so this is unreachable; the guard exists to narrow the type. */
if (!endpoint) return { status: "skipped", reason: "no_endpoint" };