mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 00:03:45 +00:00
fix(codex): require monotonic legacy refreshes
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user