fix(codex): require monotonic legacy refreshes

This commit is contained in:
Brennan Benson
2026-07-31 17:59:32 -07:00
parent 6cbed727a5
commit 4b385bfe50
2 changed files with 102 additions and 8 deletions
@@ -257,7 +257,8 @@ function createCodexAuthJson(
email: string,
accountId: string,
refreshToken: string,
expiresAt?: number
expiresAt?: number,
lastRefresh?: string
): string {
const idToken = [
encodeJwtPart({ alg: 'none', typ: 'JWT' }),
@@ -274,6 +275,7 @@ function createCodexAuthJson(
return `${JSON.stringify({
auth_mode: 'chatgpt',
...(lastRefresh === undefined ? {} : { last_refresh: lastRefresh }),
tokens: {
access_token: `access-${accountId}`,
id_token: idToken,
@@ -3767,11 +3769,19 @@ describe('CodexRuntimeHomeService', () => {
it('reads back system-default token refreshes after a pre-provenance restart', async () => {
const runtimeAuthPath = getRuntimeCodexAuthPath()
const systemAuth = createCodexAuthJson('system@example.com', 'acct-system', 'system-old')
const systemAuth = createCodexAuthJson(
'system@example.com',
'acct-system',
'system-old',
undefined,
'2026-07-30T12:00:00.000Z'
)
const refreshedAuth = createCodexAuthJson(
'system@example.com',
'acct-system',
'system-refreshed'
'system-refreshed',
undefined,
'2026-07-31T12:00:00.000Z'
)
writeFileSync(getSystemCodexAuthPath(), systemAuth, 'utf-8')
const store = createStore(createSettings())
@@ -3788,6 +3798,37 @@ describe('CodexRuntimeHomeService', () => {
expect(readFileSync(runtimeAuthPath, 'utf-8')).toBe(refreshedAuth)
})
it('does not read back older same-identity auth without provenance', async () => {
const runtimeAuthPath = getRuntimeCodexAuthPath()
const systemAuth = createCodexAuthJson(
'system@example.com',
'acct-system',
'system-newer',
undefined,
'2026-07-31T12:00:00.000Z'
)
const staleRuntimeAuth = createCodexAuthJson(
'system@example.com',
'acct-system',
'runtime-older',
undefined,
'2026-07-30T12:00:00.000Z'
)
writeFileSync(getSystemCodexAuthPath(), systemAuth, 'utf-8')
const store = createStore(createSettings())
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
new CodexRuntimeHomeService(store as never)
writeFileSync(runtimeAuthPath, staleRuntimeAuth, 'utf-8')
rmSync(getSharedRuntimeAuthProvenancePath())
const restartedService = new CodexRuntimeHomeService(store as never)
restartedService.syncForCurrentSelection()
expect(readFileSync(getSystemCodexAuthPath(), 'utf-8')).toBe(systemAuth)
expect(readFileSync(runtimeAuthPath, 'utf-8')).toBe(systemAuth)
})
it('keeps a local runtime logout when the system-default auth still exists', async () => {
const runtimeAuthPath = getRuntimeCodexAuthPath()
writeFileSync(getSystemCodexAuthPath(), '{"account":"system"}\n', 'utf-8')
@@ -3920,8 +3961,20 @@ describe('CodexRuntimeHomeService', () => {
it('clears refreshed runtime auth after a pre-provenance external logout', async () => {
const runtimeAuthPath = getRuntimeCodexAuthPath()
const systemAuth = createCodexAuthJson('system@example.com', 'acct-system', 'system')
const refreshedAuth = createCodexAuthJson('system@example.com', 'acct-system', 'refreshed')
const systemAuth = createCodexAuthJson(
'system@example.com',
'acct-system',
'system',
undefined,
'2026-07-30T12:00:00.000Z'
)
const refreshedAuth = createCodexAuthJson(
'system@example.com',
'acct-system',
'refreshed',
undefined,
'2026-07-31T12:00:00.000Z'
)
writeFileSync(getSystemCodexAuthPath(), systemAuth, 'utf-8')
const store = createStore(createSettings())
@@ -126,6 +126,42 @@ type CodexReadBackMatch =
}
| { kind: 'none' | 'ambiguous' }
function readCodexLastRefresh(authJson: string): number | null {
try {
const parsed = JSON.parse(authJson) as unknown
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
return null
}
const value = (parsed as Record<string, unknown>).last_refresh
if (typeof value === 'number') {
return Number.isFinite(value) ? value : null
}
if (typeof value !== 'string' || !value.trim()) {
return null
}
const timestamp = Date.parse(value)
return Number.isFinite(timestamp) ? timestamp : null
} catch {
return null
}
}
function codexAuthIsMonotonicallyFresher(
candidateAuthJson: string,
baselineAuthJson: string
): boolean {
const candidateLastRefresh = readCodexLastRefresh(candidateAuthJson)
const baselineLastRefresh = readCodexLastRefresh(baselineAuthJson)
if (candidateLastRefresh !== null || baselineLastRefresh !== null) {
return (
candidateLastRefresh !== null &&
baselineLastRefresh !== null &&
candidateLastRefresh > baselineLastRefresh
)
}
return codexAuthIsFresher(candidateAuthJson, baselineAuthJson)
}
export class CodexRuntimeHomeService {
// Which managed account runtime auth.json mirrors; null means it follows system-default ~/.codex instead of a managed account.
private lastSyncedAccountId: string | null = null
@@ -1593,14 +1629,19 @@ export class CodexRuntimeHomeService {
return
}
const snapshot = this.readSystemDefaultSnapshot(this.getSystemDefaultSnapshotPath())
const snapshotAuth = snapshot?.authJson ?? null
const preProvenanceRuntimeRefreshProven =
provenanceStatus.kind === 'missing' &&
snapshotAuth !== null &&
this.runtimeAuthMatchesSystemDefaultIdentity(runtimeAuth, snapshotAuth) &&
codexAuthIsMonotonicallyFresher(runtimeAuth, snapshotAuth)
const systemDefaultOwnershipProven =
provenance?.owner === 'system-default' ||
(provenanceStatus.kind === 'missing' && snapshot !== null)
provenance?.owner === 'system-default' || preProvenanceRuntimeRefreshProven
const mirroredSystemDefaultAuth =
provenance?.owner === 'system-default'
? provenance.authJson
: provenanceStatus.kind === 'missing'
? (this.lastWrittenAuthJson ?? snapshot?.authJson ?? null)
? (this.lastWrittenAuthJson ?? snapshotAuth)
: null
if (!existsSync(systemDefaultAuthPath)) {
if (mirroredSystemDefaultAuth !== null && runtimeAuth === mirroredSystemDefaultAuth) {