fix(auth): route clearClientInformation through clearAfterHydration too

Round-2 @claude review of #1592 caught a seventh member of the clear
family that the first pass missed: clearClientInformation still wrote to
the store synchronously, carrying the same pre-hydration clobber risk the
clearAfterHydration helper was written for (a clear before hydration lands
persists a near-empty blob, overwriting the whole on-disk oauth.json).
Wrap its mutation the same way and add a clear-before-hydration test
proving a sibling token (and thus every other server's blob) survives.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
This commit is contained in:
cliffhall 2026-07-01 19:26:25 -04:00
commit 4e149719bd
2 changed files with 32 additions and 10 deletions

View file

@ -172,6 +172,28 @@ describe("OAuthStorageBase — async hydration", () => {
expect(await storage.getCodeVerifier(SERVER)).toBe("keep-me");
});
it("clearClientInformation before hydration is deferred and clears only client info", async () => {
const { storage, release } = makeAsyncBackedStorage({
[SERVER]: {
clientInformation: { client_id: "abc" },
clientRegistrationKind: "dynamic",
tokens: { access_token: "t", token_type: "Bearer" },
},
});
storage.clearClientInformation(SERVER);
release();
await storage.ready();
await Promise.resolve();
// Client info cleared, but the sibling token from the persisted blob (and
// every other server's blob, in the whole-blob adapter) survives — proving
// the pre-hydration clear did not clobber on-disk state.
expect(await storage.getClientInformation(SERVER)).toBeUndefined();
expect(await storage.getTokens(SERVER)).toEqual({
access_token: "t",
token_type: "Bearer",
});
});
it("clear() after hydration does not schedule a deferred re-apply", async () => {
const { storage, release } = makeAsyncBackedStorage({
[SERVER]: { tokens: { access_token: "t", token_type: "Bearer" } },

View file

@ -172,16 +172,16 @@ export class OAuthStorageBase implements OAuthStorage {
}
clearClientInformation(serverUrl: string, isPreregistered?: boolean): void {
const updates: Partial<ServerOAuthState> = {};
if (isPreregistered) {
updates.preregisteredClientInformation = undefined;
} else {
updates.clientInformation = undefined;
updates.clientRegistrationKind = undefined;
}
this.store.getState().setServerState(serverUrl, updates);
this.clearAfterHydration(() => {
const updates: Partial<ServerOAuthState> = {};
if (isPreregistered) {
updates.preregisteredClientInformation = undefined;
} else {
updates.clientInformation = undefined;
updates.clientRegistrationKind = undefined;
}
this.store.getState().setServerState(serverUrl, updates);
});
}
async getTokens(serverUrl: string): Promise<OAuthTokens | undefined> {