source dump of claude code
at main 136 lines 4.9 kB view raw
1/** 2 * HTTP utility constants and helpers 3 */ 4 5import axios from 'axios' 6import { OAUTH_BETA_HEADER } from '../constants/oauth.js' 7import { 8 getAnthropicApiKey, 9 getClaudeAIOAuthTokens, 10 handleOAuth401Error, 11 isClaudeAISubscriber, 12} from './auth.js' 13import { getClaudeCodeUserAgent } from './userAgent.js' 14import { getWorkload } from './workloadContext.js' 15 16// WARNING: We rely on `claude-cli` in the user agent for log filtering. 17// Please do NOT change this without making sure that logging also gets updated! 18export function getUserAgent(): string { 19 const agentSdkVersion = process.env.CLAUDE_AGENT_SDK_VERSION 20 ? `, agent-sdk/${process.env.CLAUDE_AGENT_SDK_VERSION}` 21 : '' 22 // SDK consumers can identify their app/library via CLAUDE_AGENT_SDK_CLIENT_APP 23 // e.g., "my-app/1.0.0" or "my-library/2.1" 24 const clientApp = process.env.CLAUDE_AGENT_SDK_CLIENT_APP 25 ? `, client-app/${process.env.CLAUDE_AGENT_SDK_CLIENT_APP}` 26 : '' 27 // Turn-/process-scoped workload tag for cron-initiated requests. 1P-only 28 // observability — proxies strip HTTP headers; QoS routing uses cc_workload 29 // in the billing-header attribution block instead (see constants/system.ts). 30 // getAnthropicClient (client.ts:98) calls this per-request inside withRetry, 31 // so the read picks up the same setWorkload() value as getAttributionHeader. 32 const workload = getWorkload() 33 const workloadSuffix = workload ? `, workload/${workload}` : '' 34 return `claude-cli/${MACRO.VERSION} (${process.env.USER_TYPE}, ${process.env.CLAUDE_CODE_ENTRYPOINT ?? 'cli'}${agentSdkVersion}${clientApp}${workloadSuffix})` 35} 36 37export function getMCPUserAgent(): string { 38 const parts: string[] = [] 39 if (process.env.CLAUDE_CODE_ENTRYPOINT) { 40 parts.push(process.env.CLAUDE_CODE_ENTRYPOINT) 41 } 42 if (process.env.CLAUDE_AGENT_SDK_VERSION) { 43 parts.push(`agent-sdk/${process.env.CLAUDE_AGENT_SDK_VERSION}`) 44 } 45 if (process.env.CLAUDE_AGENT_SDK_CLIENT_APP) { 46 parts.push(`client-app/${process.env.CLAUDE_AGENT_SDK_CLIENT_APP}`) 47 } 48 const suffix = parts.length > 0 ? ` (${parts.join(', ')})` : '' 49 return `claude-code/${MACRO.VERSION}${suffix}` 50} 51 52// User-Agent for WebFetch requests to arbitrary sites. `Claude-User` is 53// Anthropic's publicly documented agent for user-initiated fetches (what site 54// operators match in robots.txt); the claude-code suffix lets them distinguish 55// local CLI traffic from claude.ai server-side fetches. 56export function getWebFetchUserAgent(): string { 57 return `Claude-User (${getClaudeCodeUserAgent()}; +https://support.anthropic.com/)` 58} 59 60export type AuthHeaders = { 61 headers: Record<string, string> 62 error?: string 63} 64 65/** 66 * Get authentication headers for API requests 67 * Returns either OAuth headers for Max/Pro users or API key headers for regular users 68 */ 69export function getAuthHeaders(): AuthHeaders { 70 if (isClaudeAISubscriber()) { 71 const oauthTokens = getClaudeAIOAuthTokens() 72 if (!oauthTokens?.accessToken) { 73 return { 74 headers: {}, 75 error: 'No OAuth token available', 76 } 77 } 78 return { 79 headers: { 80 Authorization: `Bearer ${oauthTokens.accessToken}`, 81 'anthropic-beta': OAUTH_BETA_HEADER, 82 }, 83 } 84 } 85 // TODO: this will fail if the API key is being set to an LLM Gateway key 86 // should we try to query keychain / credentials for a valid Anthropic key? 87 const apiKey = getAnthropicApiKey() 88 if (!apiKey) { 89 return { 90 headers: {}, 91 error: 'No API key available', 92 } 93 } 94 return { 95 headers: { 96 'x-api-key': apiKey, 97 }, 98 } 99} 100 101/** 102 * Wrapper that handles OAuth 401 errors by force-refreshing the token and 103 * retrying once. Addresses clock drift scenarios where the local expiration 104 * check disagrees with the server. 105 * 106 * The request closure is called again on retry, so it should re-read auth 107 * (e.g., via getAuthHeaders()) to pick up the refreshed token. 108 * 109 * Note: bridgeApi.ts has its own DI-injected version — handleOAuth401Error 110 * transitively pulls in config.ts (~1300 modules), which breaks the SDK bundle. 111 * 112 * @param opts.also403Revoked - Also retry on 403 with "OAuth token has been 113 * revoked" body (some endpoints signal revocation this way instead of 401). 114 */ 115export async function withOAuth401Retry<T>( 116 request: () => Promise<T>, 117 opts?: { also403Revoked?: boolean }, 118): Promise<T> { 119 try { 120 return await request() 121 } catch (err) { 122 if (!axios.isAxiosError(err)) throw err 123 const status = err.response?.status 124 const isAuthError = 125 status === 401 || 126 (opts?.also403Revoked && 127 status === 403 && 128 typeof err.response?.data === 'string' && 129 err.response.data.includes('OAuth token has been revoked')) 130 if (!isAuthError) throw err 131 const failedAccessToken = getClaudeAIOAuthTokens()?.accessToken 132 if (!failedAccessToken) throw err 133 await handleOAuth401Error(failedAccessToken) 134 return await request() 135 } 136}