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