mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +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()
|
clearOpenAIOAuthTokenCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function unsetTokenFileOverride(): void {
|
||||||
|
delete process.env.OPENAI_CODEX_OAUTH_FILE
|
||||||
|
clearOpenAIOAuthTokenCache()
|
||||||
|
}
|
||||||
|
|
||||||
test('reads desktop token file synchronously', async () => {
|
test('reads desktop token file synchronously', async () => {
|
||||||
await fsp.writeFile(
|
await fsp.writeFile(
|
||||||
tokenPath,
|
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 () => {
|
test('deletes the desktop token file when the env override is set', async () => {
|
||||||
await fsp.writeFile(
|
await fsp.writeFile(
|
||||||
tokenPath,
|
tokenPath,
|
||||||
@ -202,6 +228,10 @@ describe('OpenAI OAuth desktop token file storage', () => {
|
|||||||
expect(deleteOpenAIOAuthTokens()).toBe(true)
|
expect(deleteOpenAIOAuthTokens()).toBe(true)
|
||||||
expect(getOpenAIOAuthTokens()).toBeNull()
|
expect(getOpenAIOAuthTokens()).toBeNull()
|
||||||
await expect(getOpenAIOAuthTokensAsync()).resolves.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 () => {
|
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 fs from 'fs'
|
||||||
|
import * as os from 'os'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import memoize from 'lodash-es/memoize.js'
|
import memoize from 'lodash-es/memoize.js'
|
||||||
import { getSecureStorage } from '../../utils/secureStorage/index.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 SECURE_STORAGE_CACHE_KEY = 'secure-storage'
|
||||||
const FILE_CACHE_KEY_PREFIX = 'file:'
|
const FILE_CACHE_KEY_PREFIX = 'file:'
|
||||||
|
const FILE_BACKED_STORAGE_MARKER_FILE = 'openai-oauth-file-backed'
|
||||||
|
|
||||||
function getDesktopTokenFilePath(): string | null {
|
function getDesktopTokenFilePath(): string | null {
|
||||||
const filePath = process.env[OPENAI_CODEX_OAUTH_FILE_ENV_KEY]?.trim()
|
const filePath = process.env[OPENAI_CODEX_OAUTH_FILE_ENV_KEY]?.trim()
|
||||||
return filePath ? filePath : null
|
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 {
|
function normalizeTokenFile(value: unknown): OpenAIOAuthTokens | null {
|
||||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return null
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return null
|
||||||
|
|
||||||
@ -109,6 +144,7 @@ export function saveOpenAIOAuthTokens(tokens: OpenAIOAuthTokens): {
|
|||||||
} {
|
} {
|
||||||
try {
|
try {
|
||||||
if (writeDesktopTokenFileSync(tokens)) {
|
if (writeDesktopTokenFileSync(tokens)) {
|
||||||
|
markFileBackedStorageUsed()
|
||||||
clearOpenAIOAuthTokenCache()
|
clearOpenAIOAuthTokenCache()
|
||||||
return { success: true }
|
return { success: true }
|
||||||
}
|
}
|
||||||
@ -117,6 +153,9 @@ export function saveOpenAIOAuthTokens(tokens: OpenAIOAuthTokens): {
|
|||||||
const data = (storage.read() ?? {}) as SecureStorageShape
|
const data = (storage.read() ?? {}) as SecureStorageShape
|
||||||
data[STORAGE_KEY] = tokens
|
data[STORAGE_KEY] = tokens
|
||||||
const result = storage.update(data)
|
const result = storage.update(data)
|
||||||
|
if (result.success) {
|
||||||
|
clearFileBackedStorageMarker()
|
||||||
|
}
|
||||||
clearOpenAIOAuthTokenCache()
|
clearOpenAIOAuthTokenCache()
|
||||||
return result
|
return result
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -136,6 +175,10 @@ const getOpenAIOAuthTokensCached = memoize(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isSecureStorageFallbackDisabled()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const storage = getSecureStorage()
|
const storage = getSecureStorage()
|
||||||
const data = storage.read() as SecureStorageShape | null
|
const data = storage.read() as SecureStorageShape | null
|
||||||
@ -160,6 +203,10 @@ export async function getOpenAIOAuthTokensAsync(): Promise<OpenAIOAuthTokens | n
|
|||||||
return readDesktopTokenFileAsync(filePath)
|
return readDesktopTokenFileAsync(filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isSecureStorageFallbackDisabled()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const storage = getSecureStorage()
|
const storage = getSecureStorage()
|
||||||
const data = (await storage.readAsync()) as SecureStorageShape | null
|
const data = (await storage.readAsync()) as SecureStorageShape | null
|
||||||
@ -179,6 +226,7 @@ export function deleteOpenAIOAuthTokens(): boolean {
|
|||||||
const filePath = getDesktopTokenFilePath()
|
const filePath = getDesktopTokenFilePath()
|
||||||
if (filePath) {
|
if (filePath) {
|
||||||
fs.rmSync(filePath, { force: true })
|
fs.rmSync(filePath, { force: true })
|
||||||
|
markFileBackedStorageUsed()
|
||||||
clearOpenAIOAuthTokenCache()
|
clearOpenAIOAuthTokenCache()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user