Skip to content

Commit 093dcde

Browse files
committed
guard cli refresh token hydration
1 parent 7cb4d4f commit 093dcde

2 files changed

Lines changed: 40 additions & 2 deletions

File tree

lib/accounts.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ export class AccountManager {
191191
if (
192192
cachedAccessUsable &&
193193
cached.refreshToken &&
194-
cached.refreshToken !== account.refreshToken
194+
!account.refreshToken
195195
) {
196196
account.refreshToken = cached.refreshToken;
197197
changed = true;

test/accounts-edge.test.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ describe("accounts edge branches", () => {
182182
const snapshot = manager.getAccountsSnapshot();
183183
const updated = snapshot[0];
184184
expect(updated?.access).toBe("refreshed-access");
185-
expect(updated?.refreshToken).toBe("refreshed-refresh");
185+
expect(updated?.refreshToken).toBe("refresh-1");
186186
expect(updated?.accountId).toBe("account-from-cache");
187187
expect(updated?.accountIdSource).toBe("token");
188188

@@ -193,6 +193,44 @@ describe("accounts edge branches", () => {
193193
expect(expired?.accountIdSource).toBe("token");
194194
});
195195

196+
it("does not overwrite a local refresh token with a stale usable CLI cache token", async () => {
197+
const now = Date.now();
198+
const stored = buildStored([
199+
buildStoredAccount({
200+
refreshToken: "local-refresh-new",
201+
email: "match@example.com",
202+
accessToken: "local-access",
203+
expiresAt: now + 120_000,
204+
}),
205+
]);
206+
207+
const { AccountManager } = await importAccountsModule();
208+
const manager = new AccountManager(undefined, stored as never);
209+
210+
mockLoadCodexCliState.mockResolvedValue({
211+
sourceUpdatedAtMs: now - 60_000,
212+
accounts: [
213+
{
214+
email: "match@example.com",
215+
accessToken: "cached-access-old",
216+
expiresAt: now + 300_000,
217+
refreshToken: "cached-refresh-old",
218+
},
219+
],
220+
});
221+
222+
const hydrate = getPrivate<() => Promise<void>>(
223+
manager as object,
224+
"hydrateFromCodexCli",
225+
);
226+
await hydrate.call(manager);
227+
228+
const snapshot = manager.getAccountsSnapshot();
229+
expect(snapshot[0]?.refreshToken).toBe("local-refresh-new");
230+
expect(snapshot[0]?.access).toBe("local-access");
231+
expect(mockSaveAccounts).not.toHaveBeenCalled();
232+
});
233+
196234
it("returns early when Codex CLI state has no usable cache entries", async () => {
197235
const stored = buildStored([
198236
buildStoredAccount({

0 commit comments

Comments
 (0)