mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
Prevent legacy OpenAI OAuth token resurrection
File-backed desktop OpenAI OAuth must not later fall through to stale secure-storage credentials when a process launches without OPENAI_CODEX_OAUTH_FILE. The file-backed save and delete paths now leave a local marker that disables legacy secure-storage reads for that config directory, while an explicit secure-storage save clears the marker. Constraint: Avoid synchronous macOS keychain work on the file-backed token save path. Rejected: Delete secure-storage credentials during file-backed save | it adds keychain latency and can block the desktop OAuth write path. Confidence: high Scope-risk: narrow Tested: bun test src/services/openaiAuth/storage.test.ts src/server/__tests__/haha-openai-oauth-service.test.ts src/server/__tests__/haha-openai-oauth-api.test.ts Tested: git diff --check
This commit is contained in:
parent
98fa5a0edd
commit
22c2e203ee
@ -74,6 +74,11 @@ describe('OpenAI OAuth desktop token file storage', () => {
|
||||
clearOpenAIOAuthTokenCache()
|
||||
}
|
||||
|
||||
function unsetTokenFileOverride(): void {
|
||||
delete process.env.OPENAI_CODEX_OAUTH_FILE
|
||||
clearOpenAIOAuthTokenCache()
|
||||
}
|
||||
|
||||
test('reads desktop token file synchronously', async () => {
|
||||
await fsp.writeFile(
|
||||
tokenPath,
|
||||
@ -145,6 +150,27 @@ describe('OpenAI OAuth desktop token file storage', () => {
|
||||
}
|
||||
})
|
||||
|
||||
test('file-backed save clears legacy secure storage tokens', async () => {
|
||||
seedSecureStorage({
|
||||
accessToken: 'secure-access',
|
||||
refreshToken: 'secure-refresh',
|
||||
expiresAt: 4_100_000_000_000,
|
||||
})
|
||||
|
||||
const result = saveOpenAIOAuthTokens({
|
||||
accessToken: 'file-access',
|
||||
refreshToken: 'file-refresh',
|
||||
expiresAt: 4_100_000_000_123,
|
||||
})
|
||||
|
||||
expect(result).toEqual({ success: true })
|
||||
expect(getOpenAIOAuthTokens()?.accessToken).toBe('file-access')
|
||||
|
||||
unsetTokenFileOverride()
|
||||
expect(getOpenAIOAuthTokens()).toBeNull()
|
||||
await expect(getOpenAIOAuthTokensAsync()).resolves.toBeNull()
|
||||
})
|
||||
|
||||
test('deletes the desktop token file when the env override is set', async () => {
|
||||
await fsp.writeFile(
|
||||
tokenPath,
|
||||
@ -202,6 +228,10 @@ describe('OpenAI OAuth desktop token file storage', () => {
|
||||
expect(deleteOpenAIOAuthTokens()).toBe(true)
|
||||
expect(getOpenAIOAuthTokens()).toBeNull()
|
||||
await expect(getOpenAIOAuthTokensAsync()).resolves.toBeNull()
|
||||
|
||||
unsetTokenFileOverride()
|
||||
expect(getOpenAIOAuthTokens()).toBeNull()
|
||||
await expect(getOpenAIOAuthTokensAsync()).resolves.toBeNull()
|
||||
})
|
||||
|
||||
test('reloads sync tokens when OPENAI_CODEX_OAUTH_FILE changes without clearing cache', async () => {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import * as fs from 'fs'
|
||||
import * as os from 'os'
|
||||
import * as path from 'path'
|
||||
import memoize from 'lodash-es/memoize.js'
|
||||
import { getSecureStorage } from '../../utils/secureStorage/index.js'
|
||||
@ -15,12 +16,46 @@ type SecureStorageShape = Record<string, unknown> & {
|
||||
|
||||
const SECURE_STORAGE_CACHE_KEY = 'secure-storage'
|
||||
const FILE_CACHE_KEY_PREFIX = 'file:'
|
||||
const FILE_BACKED_STORAGE_MARKER_FILE = 'openai-oauth-file-backed'
|
||||
|
||||
function getDesktopTokenFilePath(): string | null {
|
||||
const filePath = process.env[OPENAI_CODEX_OAUTH_FILE_ENV_KEY]?.trim()
|
||||
return filePath ? filePath : null
|
||||
}
|
||||
|
||||
function getCcHahaDir(): string {
|
||||
const configDir =
|
||||
process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude')
|
||||
return path.join(configDir, 'cc-haha')
|
||||
}
|
||||
|
||||
function getFileBackedStorageMarkerPath(): string {
|
||||
return path.join(getCcHahaDir(), FILE_BACKED_STORAGE_MARKER_FILE)
|
||||
}
|
||||
|
||||
function markFileBackedStorageUsed(): void {
|
||||
try {
|
||||
fs.mkdirSync(getCcHahaDir(), { recursive: true })
|
||||
fs.writeFileSync(getFileBackedStorageMarkerPath(), '1\n', { mode: 0o600 })
|
||||
} catch (error) {
|
||||
logError(error)
|
||||
}
|
||||
}
|
||||
|
||||
function clearFileBackedStorageMarker(): void {
|
||||
try {
|
||||
fs.rmSync(getFileBackedStorageMarkerPath(), { force: true })
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
logError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isSecureStorageFallbackDisabled(): boolean {
|
||||
return fs.existsSync(getFileBackedStorageMarkerPath())
|
||||
}
|
||||
|
||||
function normalizeTokenFile(value: unknown): OpenAIOAuthTokens | null {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return null
|
||||
|
||||
@ -109,6 +144,7 @@ export function saveOpenAIOAuthTokens(tokens: OpenAIOAuthTokens): {
|
||||
} {
|
||||
try {
|
||||
if (writeDesktopTokenFileSync(tokens)) {
|
||||
markFileBackedStorageUsed()
|
||||
clearOpenAIOAuthTokenCache()
|
||||
return { success: true }
|
||||
}
|
||||
@ -117,6 +153,9 @@ export function saveOpenAIOAuthTokens(tokens: OpenAIOAuthTokens): {
|
||||
const data = (storage.read() ?? {}) as SecureStorageShape
|
||||
data[STORAGE_KEY] = tokens
|
||||
const result = storage.update(data)
|
||||
if (result.success) {
|
||||
clearFileBackedStorageMarker()
|
||||
}
|
||||
clearOpenAIOAuthTokenCache()
|
||||
return result
|
||||
} catch (error) {
|
||||
@ -136,6 +175,10 @@ const getOpenAIOAuthTokensCached = memoize(
|
||||
)
|
||||
}
|
||||
|
||||
if (isSecureStorageFallbackDisabled()) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const storage = getSecureStorage()
|
||||
const data = storage.read() as SecureStorageShape | null
|
||||
@ -160,6 +203,10 @@ export async function getOpenAIOAuthTokensAsync(): Promise<OpenAIOAuthTokens | n
|
||||
return readDesktopTokenFileAsync(filePath)
|
||||
}
|
||||
|
||||
if (isSecureStorageFallbackDisabled()) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const storage = getSecureStorage()
|
||||
const data = (await storage.readAsync()) as SecureStorageShape | null
|
||||
@ -179,6 +226,7 @@ export function deleteOpenAIOAuthTokens(): boolean {
|
||||
const filePath = getDesktopTokenFilePath()
|
||||
if (filePath) {
|
||||
fs.rmSync(filePath, { force: true })
|
||||
markFileBackedStorageUsed()
|
||||
clearOpenAIOAuthTokenCache()
|
||||
return true
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user