mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
Harden OpenAI OAuth token file writes
The desktop OpenAI OAuth token store writes plaintext refresh credentials through a temporary file before rename. A failed rename previously left that temporary file behind in the server-side store, so the writer now mirrors the CLI storage cleanup path and removes failed temp files before surfacing the original write failure. Constraint: OpenAI OAuth token files may contain refresh and id tokens. Rejected: Rely on logout cleanup | failed writes can leave orphan temp files before logout runs. 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
b10d5f7df7
commit
98fa5a0edd
@ -2,7 +2,7 @@
|
||||
* Unit tests for HahaOpenAIOAuthService — haha 自管 OpenAI OAuth 的核心 service 层。
|
||||
*/
|
||||
|
||||
import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
|
||||
import { describe, test, expect, beforeEach, afterEach, spyOn } from 'bun:test'
|
||||
import * as fs from 'fs/promises'
|
||||
import * as path from 'path'
|
||||
import * as os from 'os'
|
||||
@ -75,6 +75,36 @@ describe('HahaOpenAIOAuthService — file storage', () => {
|
||||
await service.deleteTokens()
|
||||
expect(await service.loadTokens()).toBeNull()
|
||||
})
|
||||
|
||||
test('saveTokens cleans up tmp file when rename fails', async () => {
|
||||
const renameSpy = spyOn(fs, 'rename').mockImplementation(async () => {
|
||||
const error = new Error('rename failed') as NodeJS.ErrnoException
|
||||
error.code = 'EXDEV'
|
||||
throw error
|
||||
})
|
||||
|
||||
try {
|
||||
await expect(
|
||||
service.saveTokens({
|
||||
accessToken: 'sensitive-access',
|
||||
refreshToken: 'sensitive-refresh',
|
||||
expiresAt: Date.now() + 3600_000,
|
||||
idToken: 'sensitive-id-token',
|
||||
email: 'test@example.com',
|
||||
accountId: 'acct_123',
|
||||
}),
|
||||
).rejects.toThrow('rename failed')
|
||||
} finally {
|
||||
renameSpy.mockRestore()
|
||||
}
|
||||
|
||||
const oauthPath = getHahaOpenAIOAuthFilePath()
|
||||
const files = await fs.readdir(path.dirname(oauthPath))
|
||||
expect(
|
||||
files.filter((name) => name.startsWith('openai-oauth.json.tmp.')),
|
||||
).toEqual([])
|
||||
expect(await service.loadTokens()).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('HahaOpenAIOAuthService — session management', () => {
|
||||
|
||||
@ -84,9 +84,17 @@ export class HahaOpenAIOAuthService {
|
||||
async saveTokens(tokens: StoredOpenAIOAuthTokens): Promise<void> {
|
||||
const filePath = this.getOAuthFilePath()
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
||||
const tmp = `${filePath}.tmp.${process.pid}`
|
||||
await fs.writeFile(tmp, JSON.stringify(tokens, null, 2), { mode: 0o600 })
|
||||
await fs.rename(tmp, filePath)
|
||||
const tmp = `${filePath}.tmp.${process.pid}.${Date.now()}`
|
||||
let renamed = false
|
||||
try {
|
||||
await fs.writeFile(tmp, JSON.stringify(tokens, null, 2), { mode: 0o600 })
|
||||
await fs.rename(tmp, filePath)
|
||||
renamed = true
|
||||
} finally {
|
||||
if (!renamed) {
|
||||
await fs.rm(tmp, { force: true }).catch(() => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async deleteTokens(): Promise<void> {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user