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

@ -103,18 +103,24 @@ v2/main/
│ │ # token request is made against — #2110;
│ │ # revocation.ts RFC 7009 token revocation —
│ │ # the request the three clear paths send.
│ │ # TWO halves — planOAuthRevocation then
│ │ # executeOAuthRevocation — and the order
│ │ # between them is the contract: plan →
│ │ # storage.clear() → execute. The snapshot
│ │ # must precede the clear (the token, the
│ │ # client credentials and the discovered
│ │ # `revocation_endpoint` all live in the store
│ │ # it empties); the CLEAR must precede the
│ │ # network, or a fresh authorization
│ │ # TWO halves — clearAndPlanRevocation then
│ │ # executeOAuthRevocation — and the split is
│ │ # the contract. The first TAKES the stored
│ │ # state and CLEARS it in one atomic storage
│ │ # step (OAuthStorage.takeRevocationSnapshot);
│ │ # the second sends from what was taken.
│ │ # There is no separate `storage.clear()` in
│ │ # any caller, deliberately — the clear cannot
│ │ # be forgotten or reordered. Both halves of
│ │ # that matter: the requests need state the
│ │ # clear destroys, and the clear must not wait
│ │ # on the network, or a fresh authorization
│ │ # completing during a 5s request is deleted
│ │ # by a clear reasoning about the grant it
│ │ # replaced. Names the
│ │ # replaced. NOT cross-process atomic — no
│ │ # OAuth-store mutation takes a lock, and
│ │ # `clear` alone always had that property.
│ │ # Names the
│ │ # REFRESH token when there is one (§2.1 asks
│ │ # the AS to invalidate the access tokens
│ │ # under the same grant, so one request