Add Codex OAuth provider flow for ChatGPT account sign-in (#503)
* feat: add Codex OAuth provider flow * fix: harden Codex OAuth storage, session activation, and UI
This commit is contained in:
committed by
GitHub
parent
252808bbd0
commit
fc7dc9ca0d
@@ -4,6 +4,7 @@ import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import test from 'node:test'
|
||||
|
||||
import { DEFAULT_CODEX_BASE_URL } from '../services/api/providerConfig.ts'
|
||||
import {
|
||||
buildStartupEnvFromProfile,
|
||||
buildAtomicChatProfileEnv,
|
||||
@@ -12,7 +13,9 @@ import {
|
||||
buildLaunchEnv,
|
||||
buildOllamaProfileEnv,
|
||||
buildOpenAIProfileEnv,
|
||||
clearPersistedCodexOAuthProfile,
|
||||
createProfileFile,
|
||||
isPersistedCodexOAuthProfile,
|
||||
maskSecretForDisplay,
|
||||
loadProfileFile,
|
||||
PROFILE_FILE_NAME,
|
||||
@@ -23,6 +26,13 @@ import {
|
||||
type ProfileFile,
|
||||
} from './providerProfile.ts'
|
||||
|
||||
function makeJwt(payload: Record<string, unknown>): string {
|
||||
const header = Buffer.from(JSON.stringify({ alg: 'none', typ: 'JWT' }))
|
||||
.toString('base64url')
|
||||
const body = Buffer.from(JSON.stringify(payload)).toString('base64url')
|
||||
return `${header}.${body}.signature`
|
||||
}
|
||||
|
||||
function profile(profile: ProfileFile['profile'], env: ProfileFile['env']): ProfileFile {
|
||||
return {
|
||||
profile,
|
||||
@@ -330,6 +340,7 @@ test('codex profiles accept explicit codex credentials', () => {
|
||||
assert.deepEqual(env, {
|
||||
OPENAI_BASE_URL: 'https://chatgpt.com/backend-api/codex',
|
||||
OPENAI_MODEL: 'codexspark',
|
||||
CODEX_CREDENTIAL_SOURCE: 'existing',
|
||||
CODEX_API_KEY: 'codex-live',
|
||||
CHATGPT_ACCOUNT_ID: 'acct_123',
|
||||
})
|
||||
@@ -417,6 +428,77 @@ test('saveProfileFile writes a profile that loadProfileFile can read back', () =
|
||||
}
|
||||
})
|
||||
|
||||
test('buildCodexProfileEnv tags OAuth-saved profiles so logout can remove them safely', () => {
|
||||
const env = buildCodexProfileEnv({
|
||||
model: 'codexplan',
|
||||
apiKey: makeJwt({
|
||||
'https://api.openai.com/auth': {
|
||||
chatgpt_account_id: 'acct_oauth',
|
||||
},
|
||||
}),
|
||||
credentialSource: 'oauth',
|
||||
processEnv: {},
|
||||
})
|
||||
|
||||
assert.deepEqual(env, {
|
||||
OPENAI_BASE_URL: DEFAULT_CODEX_BASE_URL,
|
||||
OPENAI_MODEL: 'codexplan',
|
||||
CODEX_CREDENTIAL_SOURCE: 'oauth',
|
||||
CODEX_API_KEY: makeJwt({
|
||||
'https://api.openai.com/auth': {
|
||||
chatgpt_account_id: 'acct_oauth',
|
||||
},
|
||||
}),
|
||||
CHATGPT_ACCOUNT_ID: 'acct_oauth',
|
||||
})
|
||||
})
|
||||
|
||||
test('clearPersistedCodexOAuthProfile removes only persisted Codex OAuth profiles', async () => {
|
||||
const cwd = mkdtempSync(join(tmpdir(), 'openclaude-codex-oauth-profile-'))
|
||||
|
||||
try {
|
||||
const providerProfileModule = await import(
|
||||
`./providerProfile.ts?ts=${Date.now()}-${Math.random()}`
|
||||
)
|
||||
const {
|
||||
PROFILE_FILE_NAME,
|
||||
clearPersistedCodexOAuthProfile,
|
||||
createProfileFile,
|
||||
isPersistedCodexOAuthProfile,
|
||||
loadProfileFile,
|
||||
saveProfileFile,
|
||||
} = providerProfileModule
|
||||
const oauthProfile = createProfileFile('codex', {
|
||||
OPENAI_MODEL: 'codexplan',
|
||||
OPENAI_BASE_URL: DEFAULT_CODEX_BASE_URL,
|
||||
CHATGPT_ACCOUNT_ID: 'acct_oauth',
|
||||
CODEX_CREDENTIAL_SOURCE: 'oauth',
|
||||
})
|
||||
saveProfileFile(oauthProfile, { cwd })
|
||||
|
||||
assert.equal(isPersistedCodexOAuthProfile(loadProfileFile({ cwd })), true)
|
||||
assert.equal(
|
||||
clearPersistedCodexOAuthProfile({ cwd }),
|
||||
join(cwd, PROFILE_FILE_NAME),
|
||||
)
|
||||
assert.equal(loadProfileFile({ cwd }), null)
|
||||
|
||||
const existingCredentialProfile = createProfileFile('codex', {
|
||||
OPENAI_MODEL: 'codexplan',
|
||||
OPENAI_BASE_URL: DEFAULT_CODEX_BASE_URL,
|
||||
CHATGPT_ACCOUNT_ID: 'acct_existing',
|
||||
CODEX_CREDENTIAL_SOURCE: 'existing',
|
||||
})
|
||||
saveProfileFile(existingCredentialProfile, { cwd })
|
||||
|
||||
assert.equal(isPersistedCodexOAuthProfile(loadProfileFile({ cwd })), false)
|
||||
assert.equal(clearPersistedCodexOAuthProfile({ cwd }), null)
|
||||
assert.deepEqual(loadProfileFile({ cwd }), existingCredentialProfile)
|
||||
} finally {
|
||||
rmSync(cwd, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test('buildStartupEnvFromProfile applies persisted gemini settings when no provider is explicitly selected', async () => {
|
||||
const env = await buildStartupEnvFromProfile({
|
||||
persisted: profile('gemini', {
|
||||
|
||||
Reference in New Issue
Block a user