http.ts 4.8 KB

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