mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +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 层。
|
* 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 fs from 'fs/promises'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as os from 'os'
|
import * as os from 'os'
|
||||||
@ -75,6 +75,36 @@ describe('HahaOpenAIOAuthService — file storage', () => {
|
|||||||
await service.deleteTokens()
|
await service.deleteTokens()
|
||||||
expect(await service.loadTokens()).toBeNull()
|
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', () => {
|
describe('HahaOpenAIOAuthService — session management', () => {
|
||||||
|
|||||||
@ -84,9 +84,17 @@ export class HahaOpenAIOAuthService {
|
|||||||
async saveTokens(tokens: StoredOpenAIOAuthTokens): Promise<void> {
|
async saveTokens(tokens: StoredOpenAIOAuthTokens): Promise<void> {
|
||||||
const filePath = this.getOAuthFilePath()
|
const filePath = this.getOAuthFilePath()
|
||||||
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
||||||
const tmp = `${filePath}.tmp.${process.pid}`
|
const tmp = `${filePath}.tmp.${process.pid}.${Date.now()}`
|
||||||
await fs.writeFile(tmp, JSON.stringify(tokens, null, 2), { mode: 0o600 })
|
let renamed = false
|
||||||
await fs.rename(tmp, filePath)
|
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> {
|
async deleteTokens(): Promise<void> {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user