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:
parent
463227c947
commit
579a3eeeb9
1 changed files with 128 additions and 20 deletions
|
|
@ -932,6 +932,54 @@ describe("revokeStoredOAuthTokens (plan + execute)", () => {
|
|||
expect(outcome).toMatchObject({ status: "failed" });
|
||||
});
|
||||
|
||||
// The preconfigured registration is only a *fallback*, so a malformed one
|
||||
// must not abort grants that carry their own valid credentials.
|
||||
it("still revokes when the preconfigured client registration is malformed", async () => {
|
||||
stubSnapshot(storage, {
|
||||
byIssuer: {
|
||||
"https://as.example.com": {
|
||||
tokens: {
|
||||
access_token: "a",
|
||||
token_type: "Bearer",
|
||||
refresh_token: "r-good",
|
||||
},
|
||||
clientInformation: { client_id: "dcr-cid" },
|
||||
},
|
||||
},
|
||||
// Not a valid `OAuthClientInformation` — no `client_id`.
|
||||
preregisteredClientInformation: { client_secret: "orphan" },
|
||||
serverMetadata: {
|
||||
issuer: "https://as.example.com",
|
||||
authorization_endpoint: "https://as.example.com/authorize",
|
||||
token_endpoint: "https://as.example.com/token",
|
||||
revocation_endpoint: REVOKE_URL,
|
||||
response_types_supported: ["code"],
|
||||
},
|
||||
});
|
||||
const fetchFn = vi.fn<typeof fetch>(
|
||||
async () => new Response(null, { status: 200 }),
|
||||
);
|
||||
|
||||
const outcome = await revokeStoredOAuthTokens({
|
||||
serverUrl: SERVER_URL,
|
||||
storage,
|
||||
fetchFn,
|
||||
});
|
||||
|
||||
// The grant was still revoked, with its OWN registration...
|
||||
expect(fetchFn).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
new URLSearchParams(String(fetchFn.mock.calls[0]![1]!.body)).get(
|
||||
"client_id",
|
||||
),
|
||||
).toBe("dcr-cid");
|
||||
// ...and the malformed fallback is reported rather than swallowed.
|
||||
expect(outcome).toMatchObject({ status: "failed" });
|
||||
expect(outcome.status === "failed" ? outcome.detail : "").toContain(
|
||||
"preconfigured client registration",
|
||||
);
|
||||
});
|
||||
|
||||
// A corrupt slot must not abandon the grants that are still revocable — the
|
||||
// state is gone either way, so the failure has to be reported BESIDE the
|
||||
// successes rather than instead of them.
|
||||
|
|
|
|||
Loading…
Reference in a new issue