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

@ -333,6 +333,30 @@ describe("clearStoredAuthForRelogin", () => {
// A grant still live at the authorization server is what the user needs to
// hear about; reporting the earlier success would print no warning at all.
// One budget across both keys, so a first request that outlives it fails
// rather than each key getting a fresh five seconds.
it("bounds both keys with one shared budget", async () => {
seedBothSpellings("live-r", "stale-r");
const fetchSpy = vi
.spyOn(globalThis, "fetch")
.mockImplementation(async () => {
// Outlives the injected budget, so the second key is never attempted.
await new Promise((r) => setTimeout(r, 40));
return new Response(null, { status: 200 });
});
try {
const outcome = await clearStoredAuthForRelogin("https://example.com", {
budgetMs: 20,
});
expect(outcome).toMatchObject({ status: "failed" });
expect(outcome?.status === "failed" ? outcome.detail : "").toContain(
"timed out after 20ms",
);
} finally {
fetchSpy.mockRestore();
}
});
it("reports a later failure over an earlier success", async () => {
seedBothSpellings("live-r", "stale-r");
const fetchSpy = vi