142 lines
4.5 KiB
TypeScript
142 lines
4.5 KiB
TypeScript
import axios from 'axios'
|
|
import isEqual from 'lodash-es/isEqual.js'
|
|
import {
|
|
getAnthropicApiKey,
|
|
getClaudeAIOAuthTokens,
|
|
hasProfileScope,
|
|
} from 'src/utils/auth.js'
|
|
import { z } from 'zod'
|
|
import { getOauthConfig, OAUTH_BETA_HEADER } from '../../constants/oauth.js'
|
|
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
|
|
import { logForDebugging } from '../../utils/debug.js'
|
|
import { withOAuth401Retry } from '../../utils/http.js'
|
|
import { lazySchema } from '../../utils/lazySchema.js'
|
|
import { logError } from '../../utils/log.js'
|
|
import { getAPIProvider } from '../../utils/model/providers.js'
|
|
import { isEssentialTrafficOnly } from '../../utils/privacyLevel.js'
|
|
import { getClaudeCodeUserAgent } from '../../utils/userAgent.js'
|
|
|
|
const bootstrapResponseSchema = lazySchema(() =>
|
|
z.object({
|
|
client_data: z.record(z.unknown()).nullish(),
|
|
additional_model_options: z
|
|
.array(
|
|
z
|
|
.object({
|
|
model: z.string(),
|
|
name: z.string(),
|
|
description: z.string(),
|
|
})
|
|
.transform(({ model, name, description }) => ({
|
|
value: model,
|
|
label: name,
|
|
description,
|
|
})),
|
|
)
|
|
.nullish(),
|
|
}),
|
|
)
|
|
|
|
type BootstrapResponse = z.infer<ReturnType<typeof bootstrapResponseSchema>>
|
|
|
|
async function fetchBootstrapAPI(): Promise<BootstrapResponse | null> {
|
|
if (isEssentialTrafficOnly()) {
|
|
logForDebugging('[Bootstrap] Skipped: Nonessential traffic disabled')
|
|
return null
|
|
}
|
|
|
|
if (getAPIProvider() !== 'firstParty') {
|
|
logForDebugging('[Bootstrap] Skipped: 3P provider')
|
|
return null
|
|
}
|
|
|
|
// OAuth preferred (requires user:profile scope — service-key OAuth tokens
|
|
// lack it and would 403). Fall back to API key auth for console users.
|
|
const apiKey = getAnthropicApiKey()
|
|
const hasUsableOAuth =
|
|
getClaudeAIOAuthTokens()?.accessToken && hasProfileScope()
|
|
if (!hasUsableOAuth && !apiKey) {
|
|
logForDebugging('[Bootstrap] Skipped: no usable OAuth or API key')
|
|
return null
|
|
}
|
|
|
|
const endpoint = `${getOauthConfig().BASE_API_URL}/api/claude_cli/bootstrap`
|
|
|
|
// withOAuth401Retry handles the refresh-and-retry. API key users fail
|
|
// through on 401 (no refresh mechanism — no OAuth token to pass).
|
|
try {
|
|
return await withOAuth401Retry(async () => {
|
|
// Re-read OAuth each call so the retry picks up the refreshed token.
|
|
const token = getClaudeAIOAuthTokens()?.accessToken
|
|
let authHeaders: Record<string, string>
|
|
if (token && hasProfileScope()) {
|
|
authHeaders = {
|
|
Authorization: `Bearer ${token}`,
|
|
'anthropic-beta': OAUTH_BETA_HEADER,
|
|
}
|
|
} else if (apiKey) {
|
|
authHeaders = { 'x-api-key': apiKey }
|
|
} else {
|
|
logForDebugging('[Bootstrap] No auth available on retry, aborting')
|
|
return null
|
|
}
|
|
|
|
logForDebugging('[Bootstrap] Fetching')
|
|
const response = await axios.get<unknown>(endpoint, {
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'User-Agent': getClaudeCodeUserAgent(),
|
|
...authHeaders,
|
|
},
|
|
timeout: 5000,
|
|
})
|
|
const parsed = bootstrapResponseSchema().safeParse(response.data)
|
|
if (!parsed.success) {
|
|
logForDebugging(
|
|
`[Bootstrap] Response failed validation: ${parsed.error.message}`,
|
|
)
|
|
return null
|
|
}
|
|
logForDebugging('[Bootstrap] Fetch ok')
|
|
return parsed.data
|
|
})
|
|
} catch (error) {
|
|
logForDebugging(
|
|
`[Bootstrap] Fetch failed: ${axios.isAxiosError(error) ? (error.response?.status ?? error.code) : 'unknown'}`,
|
|
)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch bootstrap data from the API and persist to disk cache.
|
|
*/
|
|
export async function fetchBootstrapData(): Promise<void> {
|
|
try {
|
|
const response = await fetchBootstrapAPI()
|
|
if (!response) return
|
|
|
|
const clientData = response.client_data ?? null
|
|
const additionalModelOptions = response.additional_model_options ?? []
|
|
|
|
// Only persist if data actually changed — avoids a config write on every startup.
|
|
const config = getGlobalConfig()
|
|
if (
|
|
isEqual(config.clientDataCache, clientData) &&
|
|
isEqual(config.additionalModelOptionsCache, additionalModelOptions)
|
|
) {
|
|
logForDebugging('[Bootstrap] Cache unchanged, skipping write')
|
|
return
|
|
}
|
|
|
|
logForDebugging('[Bootstrap] Cache updated, persisting to disk')
|
|
saveGlobalConfig(current => ({
|
|
...current,
|
|
clientDataCache: clientData,
|
|
additionalModelOptionsCache: additionalModelOptions,
|
|
}))
|
|
} catch (error) {
|
|
logError(error)
|
|
}
|
|
}
|