mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +08:00
When OPENAI_CODEX_OAUTH_FILE is set, the override file must stay the only source of truth. Reads now short-circuit to that file path, sync caching keys off the resolved override path, and failed atomic writes remove their tmp files before returning control. Constraint: OPENAI_CODEX_OAUTH_FILE must override shared secure storage even when the file is missing or corrupt Rejected: Keep zero-arg memoization and rely on manual cache clears | callers can switch override paths without touching the cache Confidence: high Scope-risk: narrow Directive: Do not reintroduce secure-storage fallback while the override env var is set Tested: bun test src/services/openaiAuth/storage.test.ts src/server/__tests__/haha-openai-oauth-service.test.ts 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: bun run check:server Tested: git diff --check
193 lines
5.2 KiB
TypeScript
193 lines
5.2 KiB
TypeScript
import * as fs from 'fs'
|
|
import * as path from 'path'
|
|
import memoize from 'lodash-es/memoize.js'
|
|
import { getSecureStorage } from '../../utils/secureStorage/index.js'
|
|
import { errorMessage } from '../../utils/errors.js'
|
|
import { logError } from '../../utils/log.js'
|
|
import type { OpenAIOAuthTokens } from './types.js'
|
|
|
|
const STORAGE_KEY = 'openaiCodexOauth'
|
|
export const OPENAI_CODEX_OAUTH_FILE_ENV_KEY = 'OPENAI_CODEX_OAUTH_FILE'
|
|
|
|
type SecureStorageShape = Record<string, unknown> & {
|
|
openaiCodexOauth?: OpenAIOAuthTokens
|
|
}
|
|
|
|
const SECURE_STORAGE_CACHE_KEY = '__secure-storage__'
|
|
|
|
function getDesktopTokenFilePath(): string | null {
|
|
const filePath = process.env[OPENAI_CODEX_OAUTH_FILE_ENV_KEY]?.trim()
|
|
return filePath ? filePath : null
|
|
}
|
|
|
|
function normalizeTokenFile(value: unknown): OpenAIOAuthTokens | null {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return null
|
|
|
|
const record = value as Record<string, unknown>
|
|
if (
|
|
typeof record.accessToken !== 'string' ||
|
|
typeof record.refreshToken !== 'string' ||
|
|
typeof record.expiresAt !== 'number'
|
|
) {
|
|
return null
|
|
}
|
|
|
|
return {
|
|
accessToken: record.accessToken,
|
|
refreshToken: record.refreshToken,
|
|
expiresAt: record.expiresAt,
|
|
...(typeof record.idToken === 'string' && { idToken: record.idToken }),
|
|
...(typeof record.accountId === 'string' && {
|
|
accountId: record.accountId,
|
|
}),
|
|
...(typeof record.email === 'string' && { email: record.email }),
|
|
...(typeof record.clientId === 'string' && { clientId: record.clientId }),
|
|
}
|
|
}
|
|
|
|
function readDesktopTokenFileSync(filePath = getDesktopTokenFilePath()): OpenAIOAuthTokens | null {
|
|
if (!filePath) return null
|
|
|
|
try {
|
|
return normalizeTokenFile(JSON.parse(fs.readFileSync(filePath, 'utf-8')))
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
logError(error)
|
|
}
|
|
return null
|
|
}
|
|
}
|
|
|
|
async function readDesktopTokenFileAsync(
|
|
filePath = getDesktopTokenFilePath(),
|
|
): Promise<OpenAIOAuthTokens | null> {
|
|
if (!filePath) return null
|
|
|
|
try {
|
|
const raw = await fs.promises.readFile(filePath, 'utf-8')
|
|
return normalizeTokenFile(JSON.parse(raw))
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
logError(error)
|
|
}
|
|
return null
|
|
}
|
|
}
|
|
|
|
function writeDesktopTokenFileSync(tokens: OpenAIOAuthTokens): boolean {
|
|
const filePath = getDesktopTokenFilePath()
|
|
if (!filePath) return false
|
|
|
|
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
const tmpFile = `${filePath}.tmp.${process.pid}.${Date.now()}`
|
|
let renamed = false
|
|
|
|
try {
|
|
fs.writeFileSync(tmpFile, JSON.stringify(tokens, null, 2) + '\n', {
|
|
mode: 0o600,
|
|
})
|
|
fs.renameSync(tmpFile, filePath)
|
|
renamed = true
|
|
return true
|
|
} finally {
|
|
if (!renamed) {
|
|
try {
|
|
fs.rmSync(tmpFile, { force: true })
|
|
} catch (cleanupError) {
|
|
if ((cleanupError as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
logError(cleanupError)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export function saveOpenAIOAuthTokens(tokens: OpenAIOAuthTokens): {
|
|
success: boolean
|
|
warning?: string
|
|
} {
|
|
try {
|
|
if (writeDesktopTokenFileSync(tokens)) {
|
|
clearOpenAIOAuthTokenCache()
|
|
return { success: true }
|
|
}
|
|
|
|
const storage = getSecureStorage()
|
|
const data = (storage.read() ?? {}) as SecureStorageShape
|
|
data[STORAGE_KEY] = tokens
|
|
const result = storage.update(data)
|
|
clearOpenAIOAuthTokenCache()
|
|
return result
|
|
} catch (error) {
|
|
logError(error)
|
|
return {
|
|
success: false,
|
|
warning: `Failed to save OpenAI OAuth tokens: ${errorMessage(error)}`,
|
|
}
|
|
}
|
|
}
|
|
|
|
const getOpenAIOAuthTokensCached = memoize(
|
|
(cacheKey: string): OpenAIOAuthTokens | null => {
|
|
if (cacheKey !== SECURE_STORAGE_CACHE_KEY) {
|
|
return readDesktopTokenFileSync(cacheKey)
|
|
}
|
|
|
|
try {
|
|
const storage = getSecureStorage()
|
|
const data = storage.read() as SecureStorageShape | null
|
|
return data?.openaiCodexOauth ?? null
|
|
} catch (error) {
|
|
logError(error)
|
|
return null
|
|
}
|
|
},
|
|
)
|
|
|
|
export function getOpenAIOAuthTokens(): OpenAIOAuthTokens | null {
|
|
return getOpenAIOAuthTokensCached(
|
|
getDesktopTokenFilePath() ?? SECURE_STORAGE_CACHE_KEY,
|
|
)
|
|
}
|
|
|
|
export async function getOpenAIOAuthTokensAsync(): Promise<OpenAIOAuthTokens | null> {
|
|
const filePath = getDesktopTokenFilePath()
|
|
if (filePath) {
|
|
return readDesktopTokenFileAsync(filePath)
|
|
}
|
|
|
|
try {
|
|
const storage = getSecureStorage()
|
|
const data = (await storage.readAsync()) as SecureStorageShape | null
|
|
return data?.openaiCodexOauth ?? null
|
|
} catch (error) {
|
|
logError(error)
|
|
return null
|
|
}
|
|
}
|
|
|
|
export function clearOpenAIOAuthTokenCache(): void {
|
|
getOpenAIOAuthTokensCached.cache?.clear?.()
|
|
}
|
|
|
|
export function deleteOpenAIOAuthTokens(): boolean {
|
|
try {
|
|
const filePath = getDesktopTokenFilePath()
|
|
if (filePath) {
|
|
fs.rmSync(filePath, { force: true })
|
|
clearOpenAIOAuthTokenCache()
|
|
return true
|
|
}
|
|
|
|
const storage = getSecureStorage()
|
|
const data = (storage.read() ?? {}) as SecureStorageShape
|
|
delete data[STORAGE_KEY]
|
|
const result = storage.update(data)
|
|
clearOpenAIOAuthTokenCache()
|
|
return result.success
|
|
} catch (error) {
|
|
logError(error)
|
|
return false
|
|
}
|
|
}
|