fix(desktop): cleanup HahaOAuthService — 删除 dead code、加 fetch timeout、log refresh errors

Code review follow-ups on b0ca5861:
- 删除 dead code exchangeFn/setExchangeFn (completeSession 总走 exchangeWithCustomCallback)
- exchangeWithCustomCallback fetch 加 15s AbortController timeout (parity with client.ts)
- ensureFreshAccessToken refresh 失败时 console.error,不再静默吞异常
- test 删除未用的 mock import
- getOauthConfig 改为 static import
- saveTokens 加注释说明 atomic write 意图

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
程序员阿江(Relakkes) 2026-04-17 19:55:12 +08:00
parent b0ca586190
commit a1b2530fab
2 changed files with 22 additions and 21 deletions

View File

@ -2,7 +2,7 @@
* Unit tests for HahaOAuthService haha OAuth service
*/
import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test'
import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
import * as fs from 'fs/promises'
import * as path from 'path'
import * as os from 'os'

View File

@ -19,7 +19,6 @@ import {
} from '../../services/oauth/crypto.js'
import {
buildAuthUrl,
exchangeCodeForTokens,
refreshOAuthToken,
isOAuthTokenExpired,
parseScopes,
@ -29,6 +28,7 @@ import type {
OAuthTokenExchangeResponse,
SubscriptionType,
} from '../../services/oauth/types.js'
import { getOauthConfig } from '../../constants/oauth.js'
export type StoredOAuthTokens = {
accessToken: string
@ -47,29 +47,17 @@ export type OAuthSession = {
}
type RefreshFn = (refreshToken: string, opts?: { scopes?: string[] }) => Promise<OAuthTokens>
type ExchangeFn = (
code: string,
state: string,
verifier: string,
port: number,
) => Promise<OAuthTokenExchangeResponse>
const SESSION_TTL_MS = 5 * 60 * 1000
export class HahaOAuthService {
private sessions = new Map<string, OAuthSession>()
private refreshFn: RefreshFn = refreshOAuthToken
private exchangeFn: ExchangeFn = (code, state, verifier, port) =>
exchangeCodeForTokens(code, state, verifier, port, false)
setRefreshFn(fn: RefreshFn): void {
this.refreshFn = fn
}
setExchangeFn(fn: ExchangeFn): void {
this.exchangeFn = fn
}
private getOAuthFilePath(): string {
const configDir =
process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude')
@ -89,6 +77,8 @@ export class HahaOAuthService {
async saveTokens(tokens: StoredOAuthTokens): Promise<void> {
const filePath = this.getOAuthFilePath()
await fs.mkdir(path.dirname(filePath), { recursive: true })
// 写临时文件再 rename,防止写到一半被其他读者读到残缺 JSON。
// 单进程 desktop 下 pid 后缀足够隔离。
const tmp = `${filePath}.tmp.${process.pid}`
await fs.writeFile(tmp, JSON.stringify(tokens, null, 2), { mode: 0o600 })
await fs.rename(tmp, filePath)
@ -194,7 +184,6 @@ export class HahaOAuthService {
verifier: string,
port: number,
): Promise<OAuthTokenExchangeResponse> {
const { getOauthConfig } = await import('../../constants/oauth.js')
const requestBody = {
grant_type: 'authorization_code',
code,
@ -204,11 +193,19 @@ export class HahaOAuthService {
state,
}
const res = await fetch(getOauthConfig().TOKEN_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody),
})
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 15_000)
let res: Response
try {
res = await fetch(getOauthConfig().TOKEN_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody),
signal: controller.signal,
})
} finally {
clearTimeout(timeoutId)
}
if (!res.ok) {
throw new Error(
`Token exchange failed (${res.status}): ${await res.text()}`,
@ -240,7 +237,11 @@ export class HahaOAuthService {
}
await this.saveTokens(updated)
return updated.accessToken
} catch {
} catch (err) {
console.error(
'[HahaOAuthService] token refresh failed:',
err instanceof Error ? err.message : err,
)
return null
}
}