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:
parent
3c65bdb4c6
commit
af174666d6
4 changed files with 31 additions and 993 deletions
988
clients/launcher/package-lock.json
generated
988
clients/launcher/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -21,6 +21,16 @@ describe("parseOAuthPersistBlob", () => {
|
||||||
expect(parseOAuthPersistBlob(JSON.stringify(snapshot))).toEqual(snapshot);
|
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", () => {
|
it("promotes legacy persist envelope state to the top level", () => {
|
||||||
const legacy = {
|
const legacy = {
|
||||||
state: {
|
state: {
|
||||||
|
|
|
||||||
|
|
@ -36,14 +36,22 @@ function snapshotFromPayload(payload: Partial<OAuthPersistSnapshot>): OAuthPersi
|
||||||
/**
|
/**
|
||||||
* Parse OAuth store JSON from disk, remote API, or sessionStorage.
|
* Parse OAuth store JSON from disk, remote API, or sessionStorage.
|
||||||
* Accepts plain `{ servers, idpSessions }` or legacy `{ state, version }`.
|
* 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(
|
export function parseOAuthPersistBlob(
|
||||||
raw: string | null,
|
raw: string | null | unknown,
|
||||||
): OAuthPersistSnapshot | null {
|
): OAuthPersistSnapshot | null {
|
||||||
if (!raw) {
|
if (raw === null || raw === undefined) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const parsed = parseStore(raw);
|
|
||||||
|
const parsed =
|
||||||
|
typeof raw === "string"
|
||||||
|
? raw
|
||||||
|
? parseStore(raw)
|
||||||
|
: null
|
||||||
|
: raw;
|
||||||
|
|
||||||
if (!isRecord(parsed)) {
|
if (!isRecord(parsed)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -131,7 +139,7 @@ export function createRemoteOAuthPersistBackend(
|
||||||
if (Object.keys(store as object).length === 0) {
|
if (Object.keys(store as object).length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return parseOAuthPersistBlob(JSON.stringify(store));
|
return parseOAuthPersistBlob(store);
|
||||||
},
|
},
|
||||||
async write(snapshot) {
|
async write(snapshot) {
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ import type {
|
||||||
export class OAuthStorageBase implements OAuthStorage {
|
export class OAuthStorageBase implements OAuthStorage {
|
||||||
private loaded = false;
|
private loaded = false;
|
||||||
private loadPromise: Promise<void> | undefined;
|
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 memory: OAuthMemoryStore;
|
||||||
private readonly backend: OAuthPersistBackend;
|
private readonly backend: OAuthPersistBackend;
|
||||||
|
|
||||||
|
|
@ -55,7 +57,13 @@ export class OAuthStorageBase implements OAuthStorage {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async persist(): Promise<void> {
|
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(
|
async getClientInformation(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue