mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +08:00
Electron desktop runs network-sensitive OpenAI OAuth token exchange in the sidecar process, so the sidecar now receives system proxy env derived from Electron's cross-platform proxy resolver and the OAuth token client uses the existing proxy fetch options. General manual proxy settings also document and preserve authenticated proxy URLs. The macOS fullscreen black-screen path is addressed by avoiding native fullscreen Spaces for app fullscreen toggles and by leaving fullscreen before hiding or closing the window. Constraint: Electron packaged apps may not inherit shell HTTPS_PROXY env when launched from Finder. Constraint: Manual authenticated proxies must remain standard HTTP(S) proxy URLs for Bun/undici compatibility. Rejected: Store proxy username and password as separate fields | would require new secret-storage semantics and migration beyond this bugfix. Rejected: Use native macOS fullscreen Spaces for the desktop app | reproduced black-screen behavior when hiding/closing from fullscreen. Confidence: high Scope-risk: moderate Directive: Do not remove sidecar proxy env injection without retesting OpenAI OAuth from a packaged app launched outside a shell. Tested: bun test src/services/openaiAuth/client.test.ts src/server/__tests__/haha-openai-oauth-service.test.ts Tested: bun test src/server/__tests__/network-settings.test.ts Tested: cd desktop && bun run test -- src/__tests__/generalSettings.test.tsx --run Tested: cd desktop && bun run check:electron Tested: git diff --check Not-tested: bun run check:server blocked by expired quarantine entries server:cron-scheduler, server:providers-real, server:tasks, server:e2e:business-flow, server:e2e:full-flow Not-tested: live OpenAI OAuth through a corporate authenticated proxy
213 lines
6.9 KiB
TypeScript
213 lines
6.9 KiB
TypeScript
import { describe, expect, test } from 'bun:test'
|
|
import {
|
|
buildOpenAIAuthorizeUrl,
|
|
exchangeOpenAICodeForTokens,
|
|
generateOpenAICodeVerifier,
|
|
generateOpenAIState,
|
|
OPENAI_CODEX_CLIENT_ID,
|
|
OPENAI_CODEX_TOKEN_USER_AGENT,
|
|
refreshOpenAITokens,
|
|
} from './client.js'
|
|
|
|
describe('OpenAI Codex OAuth client', () => {
|
|
test('generates hex PKCE verifier and state like the Codex-compatible flow', () => {
|
|
const verifier = generateOpenAICodeVerifier()
|
|
const state = generateOpenAIState()
|
|
|
|
expect(verifier).toMatch(/^[a-f0-9]{128}$/)
|
|
expect(state).toMatch(/^[a-f0-9]{64}$/)
|
|
})
|
|
|
|
test('builds authorize URL without non-Codex originator parameter', () => {
|
|
const authorizeUrl = buildOpenAIAuthorizeUrl({
|
|
redirectUri: 'http://localhost:1455/auth/callback',
|
|
codeVerifier: 'a'.repeat(128),
|
|
state: 'b'.repeat(64),
|
|
})
|
|
const parsed = new URL(authorizeUrl)
|
|
const params = parsed.searchParams
|
|
|
|
expect(parsed.origin + parsed.pathname).toBe(
|
|
'https://auth.openai.com/oauth/authorize',
|
|
)
|
|
expect(params.get('client_id')).toBe(OPENAI_CODEX_CLIENT_ID)
|
|
expect(params.get('redirect_uri')).toBe(
|
|
'http://localhost:1455/auth/callback',
|
|
)
|
|
expect(params.get('scope')).toBe('openid profile email offline_access')
|
|
expect(params.get('code_challenge_method')).toBe('S256')
|
|
expect(params.get('id_token_add_organizations')).toBe('true')
|
|
expect(params.get('codex_cli_simplified_flow')).toBe('true')
|
|
expect(params.has('originator')).toBe(false)
|
|
})
|
|
|
|
test('exchanges authorization code with Codex-compatible token request headers', async () => {
|
|
const originalFetch = globalThis.fetch
|
|
let tokenRequestUrl = ''
|
|
let tokenRequestBody = ''
|
|
let tokenRequestHeaders = new Headers()
|
|
|
|
globalThis.fetch = (async (input, init) => {
|
|
tokenRequestUrl = String(input)
|
|
tokenRequestBody = String(init?.body ?? '')
|
|
tokenRequestHeaders = new Headers(init?.headers)
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
access_token: 'access-token',
|
|
refresh_token: 'refresh-token',
|
|
expires_in: 3600,
|
|
}),
|
|
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
)
|
|
}) as typeof fetch
|
|
|
|
try {
|
|
await exchangeOpenAICodeForTokens({
|
|
code: 'auth-code',
|
|
redirectUri: 'http://localhost:1455/auth/callback',
|
|
codeVerifier: 'verifier',
|
|
})
|
|
|
|
expect(tokenRequestUrl).toBe('https://auth.openai.com/oauth/token')
|
|
expect(tokenRequestHeaders.get('Accept')).toBe('application/json')
|
|
expect(tokenRequestHeaders.get('Content-Type')).toBe(
|
|
'application/x-www-form-urlencoded',
|
|
)
|
|
expect(tokenRequestHeaders.get('User-Agent')).toBe(
|
|
OPENAI_CODEX_TOKEN_USER_AGENT,
|
|
)
|
|
expect(tokenRequestBody).toContain('grant_type=authorization_code')
|
|
expect(tokenRequestBody).toContain('client_id=app_EMoamEEZ73f0CkXaXp7hrann')
|
|
expect(tokenRequestBody).toContain('code=auth-code')
|
|
expect(tokenRequestBody).toContain(
|
|
`redirect_uri=${encodeURIComponent('http://localhost:1455/auth/callback')}`,
|
|
)
|
|
expect(tokenRequestBody).toContain('code_verifier=verifier')
|
|
} finally {
|
|
globalThis.fetch = originalFetch
|
|
}
|
|
})
|
|
|
|
test('exchanges authorization code through configured proxy fetch options', async () => {
|
|
const originalFetch = globalThis.fetch
|
|
let tokenRequestInit: RequestInit | undefined
|
|
|
|
globalThis.fetch = (async (_input, init) => {
|
|
tokenRequestInit = init
|
|
return new Response(
|
|
JSON.stringify({
|
|
access_token: 'access-token',
|
|
refresh_token: 'refresh-token',
|
|
expires_in: 3600,
|
|
}),
|
|
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
)
|
|
}) as typeof fetch
|
|
|
|
try {
|
|
await exchangeOpenAICodeForTokens({
|
|
code: 'auth-code',
|
|
redirectUri: 'http://localhost:1455/auth/callback',
|
|
codeVerifier: 'verifier',
|
|
proxyUrl: 'http://127.0.0.1:7890',
|
|
timeoutMs: 30_000,
|
|
})
|
|
|
|
expect((tokenRequestInit as { proxy?: string } | undefined)?.proxy).toBe(
|
|
'http://127.0.0.1:7890',
|
|
)
|
|
expect(tokenRequestInit?.signal).toBeInstanceOf(AbortSignal)
|
|
} finally {
|
|
globalThis.fetch = originalFetch
|
|
}
|
|
})
|
|
|
|
test('refreshes tokens with Codex-compatible token request headers', async () => {
|
|
const originalFetch = globalThis.fetch
|
|
let tokenRequestBody = ''
|
|
let tokenRequestHeaders = new Headers()
|
|
|
|
globalThis.fetch = (async (_input, init) => {
|
|
tokenRequestBody = String(init?.body ?? '')
|
|
tokenRequestHeaders = new Headers(init?.headers)
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
access_token: 'access-token',
|
|
expires_in: 3600,
|
|
}),
|
|
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
)
|
|
}) as typeof fetch
|
|
|
|
try {
|
|
await refreshOpenAITokens('refresh-token')
|
|
|
|
expect(tokenRequestHeaders.get('User-Agent')).toBe(
|
|
OPENAI_CODEX_TOKEN_USER_AGENT,
|
|
)
|
|
expect(tokenRequestBody).toContain('grant_type=refresh_token')
|
|
expect(tokenRequestBody).toContain('refresh_token=refresh-token')
|
|
expect(tokenRequestBody).toContain('scope=openid+profile+email')
|
|
} finally {
|
|
globalThis.fetch = originalFetch
|
|
}
|
|
})
|
|
|
|
test('refreshes tokens through configured proxy fetch options', async () => {
|
|
const originalFetch = globalThis.fetch
|
|
let tokenRequestInit: RequestInit | undefined
|
|
|
|
globalThis.fetch = (async (_input, init) => {
|
|
tokenRequestInit = init
|
|
return new Response(
|
|
JSON.stringify({
|
|
access_token: 'access-token',
|
|
expires_in: 3600,
|
|
}),
|
|
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
)
|
|
}) as typeof fetch
|
|
|
|
try {
|
|
await refreshOpenAITokens('refresh-token', {
|
|
proxyUrl: 'http://127.0.0.1:7890',
|
|
timeoutMs: 30_000,
|
|
})
|
|
|
|
expect((tokenRequestInit as { proxy?: string } | undefined)?.proxy).toBe(
|
|
'http://127.0.0.1:7890',
|
|
)
|
|
expect(tokenRequestInit?.signal).toBeInstanceOf(AbortSignal)
|
|
} finally {
|
|
globalThis.fetch = originalFetch
|
|
}
|
|
})
|
|
|
|
test('includes sanitized token error response details for diagnostics', async () => {
|
|
const originalFetch = globalThis.fetch
|
|
|
|
globalThis.fetch = (async () => {
|
|
return new Response(
|
|
'{"error":"forbidden","code":"sensitive-code","refresh_token":"secret"}',
|
|
{ status: 403, headers: { 'Content-Type': 'application/json' } },
|
|
)
|
|
}) as typeof fetch
|
|
|
|
try {
|
|
await expect(
|
|
exchangeOpenAICodeForTokens({
|
|
code: 'auth-code',
|
|
redirectUri: 'http://localhost:1455/auth/callback',
|
|
codeVerifier: 'verifier',
|
|
}),
|
|
).rejects.toThrow(
|
|
'OpenAI token exchange failed: 403: {"error":"forbidden","code":"[redacted]","refresh_token":"[redacted]"}',
|
|
)
|
|
} finally {
|
|
globalThis.fetch = originalFetch
|
|
}
|
|
})
|
|
})
|