File size: 4,634 Bytes
064bfd6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | 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)
}
}
|