| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136 |
- /**
- * HTTP utility constants and helpers
- */
- import axios from 'axios'
- import { OAUTH_BETA_HEADER } from '../constants/oauth.js'
- import {
- getAnthropicApiKey,
- getClaudeAIOAuthTokens,
- handleOAuth401Error,
- isClaudeAISubscriber,
- } from './auth.js'
- import { getClaudeCodeUserAgent } from './userAgent.js'
- import { getWorkload } from './workloadContext.js'
- // WARNING: We rely on `claude-cli` in the user agent for log filtering.
- // Please do NOT change this without making sure that logging also gets updated!
- export function getUserAgent(): string {
- const agentSdkVersion = process.env.CLAUDE_AGENT_SDK_VERSION
- ? `, agent-sdk/${process.env.CLAUDE_AGENT_SDK_VERSION}`
- : ''
- // SDK consumers can identify their app/library via CLAUDE_AGENT_SDK_CLIENT_APP
- // e.g., "my-app/1.0.0" or "my-library/2.1"
- const clientApp = process.env.CLAUDE_AGENT_SDK_CLIENT_APP
- ? `, client-app/${process.env.CLAUDE_AGENT_SDK_CLIENT_APP}`
- : ''
- // Turn-/process-scoped workload tag for cron-initiated requests. 1P-only
- // observability — proxies strip HTTP headers; QoS routing uses cc_workload
- // in the billing-header attribution block instead (see constants/system.ts).
- // getAnthropicClient (client.ts:98) calls this per-request inside withRetry,
- // so the read picks up the same setWorkload() value as getAttributionHeader.
- const workload = getWorkload()
- const workloadSuffix = workload ? `, workload/${workload}` : ''
- return `claude-cli/${MACRO.VERSION} (${process.env.USER_TYPE}, ${process.env.CLAUDE_CODE_ENTRYPOINT ?? 'cli'}${agentSdkVersion}${clientApp}${workloadSuffix})`
- }
- export function getMCPUserAgent(): string {
- const parts: string[] = []
- if (process.env.CLAUDE_CODE_ENTRYPOINT) {
- parts.push(process.env.CLAUDE_CODE_ENTRYPOINT)
- }
- if (process.env.CLAUDE_AGENT_SDK_VERSION) {
- parts.push(`agent-sdk/${process.env.CLAUDE_AGENT_SDK_VERSION}`)
- }
- if (process.env.CLAUDE_AGENT_SDK_CLIENT_APP) {
- parts.push(`client-app/${process.env.CLAUDE_AGENT_SDK_CLIENT_APP}`)
- }
- const suffix = parts.length > 0 ? ` (${parts.join(', ')})` : ''
- return `claude-code/${MACRO.VERSION}${suffix}`
- }
- // User-Agent for WebFetch requests to arbitrary sites. `Claude-User` is
- // Anthropic's publicly documented agent for user-initiated fetches (what site
- // operators match in robots.txt); the claude-code suffix lets them distinguish
- // local CLI traffic from claude.ai server-side fetches.
- export function getWebFetchUserAgent(): string {
- return `Claude-User (${getClaudeCodeUserAgent()}; +https://support.anthropic.com/)`
- }
- export type AuthHeaders = {
- headers: Record<string, string>
- error?: string
- }
- /**
- * Get authentication headers for API requests
- * Returns either OAuth headers for Max/Pro users or API key headers for regular users
- */
- export function getAuthHeaders(): AuthHeaders {
- if (isClaudeAISubscriber()) {
- const oauthTokens = getClaudeAIOAuthTokens()
- if (!oauthTokens?.accessToken) {
- return {
- headers: {},
- error: 'No OAuth token available',
- }
- }
- return {
- headers: {
- Authorization: `Bearer ${oauthTokens.accessToken}`,
- 'anthropic-beta': OAUTH_BETA_HEADER,
- },
- }
- }
- // TODO: this will fail if the API key is being set to an LLM Gateway key
- // should we try to query keychain / credentials for a valid Anthropic key?
- const apiKey = getAnthropicApiKey()
- if (!apiKey) {
- return {
- headers: {},
- error: 'No API key available',
- }
- }
- return {
- headers: {
- 'x-api-key': apiKey,
- },
- }
- }
- /**
- * Wrapper that handles OAuth 401 errors by force-refreshing the token and
- * retrying once. Addresses clock drift scenarios where the local expiration
- * check disagrees with the server.
- *
- * The request closure is called again on retry, so it should re-read auth
- * (e.g., via getAuthHeaders()) to pick up the refreshed token.
- *
- * Note: bridgeApi.ts has its own DI-injected version — handleOAuth401Error
- * transitively pulls in config.ts (~1300 modules), which breaks the SDK bundle.
- *
- * @param opts.also403Revoked - Also retry on 403 with "OAuth token has been
- * revoked" body (some endpoints signal revocation this way instead of 401).
- */
- export async function withOAuth401Retry<T>(
- request: () => Promise<T>,
- opts?: { also403Revoked?: boolean },
- ): Promise<T> {
- try {
- return await request()
- } catch (err) {
- if (!axios.isAxiosError(err)) throw err
- const status = err.response?.status
- const isAuthError =
- status === 401 ||
- (opts?.also403Revoked &&
- status === 403 &&
- typeof err.response?.data === 'string' &&
- err.response.data.includes('OAuth token has been revoked'))
- if (!isAuthError) throw err
- const failedAccessToken = getClaudeAIOAuthTokens()?.accessToken
- if (!failedAccessToken) throw err
- await handleOAuth401Error(failedAccessToken)
- return await request()
- }
- }
|