fix(auth): address Claude review on OAuth persist serialization

Serialize persist() in OAuthStorageBase so overlapping remote POSTs cannot
reorder and clobber state. Regenerate launcher package-lock.json to drop
stale zustand entries. Let parseOAuthPersistBlob accept parsed objects
from res.json() without a redundant JSON round-trip.
This commit is contained in:
Bob Dickinson 2026-07-05 18:38:13 -07:00
commit af174666d6
4 changed files with 31 additions and 993 deletions

File diff suppressed because it is too large Load diff

View file

@ -21,6 +21,16 @@ describe("parseOAuthPersistBlob", () => {
expect(parseOAuthPersistBlob(JSON.stringify(snapshot))).toEqual(snapshot);
});
it("accepts an already-parsed object without re-serializing", () => {
const snapshot = {
servers: {
"http://example.com": { codeVerifier: "v1" },
},
idpSessions: {},
};
expect(parseOAuthPersistBlob(snapshot)).toEqual(snapshot);
});
it("promotes legacy persist envelope state to the top level", () => {
const legacy = {
state: {

View file

@ -36,14 +36,22 @@ function snapshotFromPayload(payload: Partial<OAuthPersistSnapshot>): OAuthPersi
/**
* Parse OAuth store JSON from disk, remote API, or sessionStorage.
* Accepts plain `{ servers, idpSessions }` or legacy `{ state, version }`.
* `raw` may be a JSON string or an already-parsed object (e.g. from `res.json()`).
*/
export function parseOAuthPersistBlob(
raw: string | null,
raw: string | null | unknown,
): OAuthPersistSnapshot | null {
if (!raw) {
if (raw === null || raw === undefined) {
return null;
}
const parsed = parseStore(raw);
const parsed =
typeof raw === "string"
? raw
? parseStore(raw)
: null
: raw;
if (!isRecord(parsed)) {
return null;
}
@ -131,7 +139,7 @@ export function createRemoteOAuthPersistBackend(
if (Object.keys(store as object).length === 0) {
return null;
}
return parseOAuthPersistBlob(JSON.stringify(store));
return parseOAuthPersistBlob(store);
},
async write(snapshot) {
const headers: Record<string, string> = {

View file

@ -24,6 +24,8 @@ import type {
export class OAuthStorageBase implements OAuthStorage {
private loaded = false;
private loadPromise: Promise<void> | undefined;
/** Serializes persist writes so concurrent mutators cannot reorder POSTs. */
private persistQueue: Promise<void> = Promise.resolve();
private readonly memory: OAuthMemoryStore;
private readonly backend: OAuthPersistBackend;
@ -55,7 +57,13 @@ export class OAuthStorageBase implements OAuthStorage {
}
private async persist(): Promise<void> {
await this.backend.write(this.memory.snapshot());
const snapshot = this.memory.snapshot();
const prior = this.persistQueue;
const tracked = prior
.catch(() => {})
.then(() => this.backend.write(snapshot));
this.persistQueue = tracked;
await tracked;
}
async getClientInformation(