/ constants / oauth.ts
oauth.ts
  1  import { isEnvTruthy } from 'src/utils/envUtils.js'
  2  
  3  // Default to prod config, override with test/staging if enabled
  4  type OauthConfigType = 'prod' | 'staging' | 'local'
  5  
  6  function getOauthConfigType(): OauthConfigType {
  7    if (process.env.USER_TYPE === 'ant') {
  8      if (isEnvTruthy(process.env.USE_LOCAL_OAUTH)) {
  9        return 'local'
 10      }
 11      if (isEnvTruthy(process.env.USE_STAGING_OAUTH)) {
 12        return 'staging'
 13      }
 14    }
 15    return 'prod'
 16  }
 17  
 18  export function fileSuffixForOauthConfig(): string {
 19    if (process.env.CLAUDE_CODE_CUSTOM_OAUTH_URL) {
 20      return '-custom-oauth'
 21    }
 22    switch (getOauthConfigType()) {
 23      case 'local':
 24        return '-local-oauth'
 25      case 'staging':
 26        return '-staging-oauth'
 27      case 'prod':
 28        // No suffix for production config
 29        return ''
 30    }
 31  }
 32  
 33  export const CLAUDE_AI_INFERENCE_SCOPE = 'user:inference' as const
 34  export const CLAUDE_AI_PROFILE_SCOPE = 'user:profile' as const
 35  const CONSOLE_SCOPE = 'org:create_api_key' as const
 36  export const OAUTH_BETA_HEADER = 'oauth-2025-04-20' as const
 37  
 38  // Console OAuth scopes - for API key creation via Console
 39  export const CONSOLE_OAUTH_SCOPES = [
 40    CONSOLE_SCOPE,
 41    CLAUDE_AI_PROFILE_SCOPE,
 42  ] as const
 43  
 44  // Claude.ai OAuth scopes - for Claude.ai subscribers (Pro/Max/Team/Enterprise)
 45  export const CLAUDE_AI_OAUTH_SCOPES = [
 46    CLAUDE_AI_PROFILE_SCOPE,
 47    CLAUDE_AI_INFERENCE_SCOPE,
 48    'user:sessions:claude_code',
 49    'user:mcp_servers',
 50    'user:file_upload',
 51  ] as const
 52  
 53  // All OAuth scopes - union of all scopes used in Claude CLI
 54  // When logging in, request all scopes in order to handle both Console -> Claude.ai redirect
 55  // Ensure that `OAuthConsentPage` in apps repo is kept in sync with this list.
 56  export const ALL_OAUTH_SCOPES = Array.from(
 57    new Set([...CONSOLE_OAUTH_SCOPES, ...CLAUDE_AI_OAUTH_SCOPES]),
 58  )
 59  
 60  type OauthConfig = {
 61    BASE_API_URL: string
 62    CONSOLE_AUTHORIZE_URL: string
 63    CLAUDE_AI_AUTHORIZE_URL: string
 64    /**
 65     * The claude.ai web origin. Separate from CLAUDE_AI_AUTHORIZE_URL because
 66     * that now routes through claude.com/cai/* for attribution — deriving
 67     * .origin from it would give claude.com, breaking links to /code,
 68     * /settings/connectors, and other claude.ai web pages.
 69     */
 70    CLAUDE_AI_ORIGIN: string
 71    TOKEN_URL: string
 72    API_KEY_URL: string
 73    ROLES_URL: string
 74    CONSOLE_SUCCESS_URL: string
 75    CLAUDEAI_SUCCESS_URL: string
 76    MANUAL_REDIRECT_URL: string
 77    CLIENT_ID: string
 78    OAUTH_FILE_SUFFIX: string
 79    MCP_PROXY_URL: string
 80    MCP_PROXY_PATH: string
 81  }
 82  
 83  // Production OAuth configuration - Used in normal operation
 84  const PROD_OAUTH_CONFIG = {
 85    BASE_API_URL: 'https://api.anthropic.com',
 86    CONSOLE_AUTHORIZE_URL: 'https://platform.claude.com/oauth/authorize',
 87    // Bounces through claude.com/cai/* so CLI sign-ins connect to claude.com
 88    // visits for attribution. 307s to claude.ai/oauth/authorize in two hops.
 89    CLAUDE_AI_AUTHORIZE_URL: 'https://claude.com/cai/oauth/authorize',
 90    CLAUDE_AI_ORIGIN: 'https://claude.ai',
 91    TOKEN_URL: 'https://platform.claude.com/v1/oauth/token',
 92    API_KEY_URL: 'https://api.anthropic.com/api/oauth/claude_cli/create_api_key',
 93    ROLES_URL: 'https://api.anthropic.com/api/oauth/claude_cli/roles',
 94    CONSOLE_SUCCESS_URL:
 95      'https://platform.claude.com/buy_credits?returnUrl=/oauth/code/success%3Fapp%3Dclaude-code',
 96    CLAUDEAI_SUCCESS_URL:
 97      'https://platform.claude.com/oauth/code/success?app=claude-code',
 98    MANUAL_REDIRECT_URL: 'https://platform.claude.com/oauth/code/callback',
 99    CLIENT_ID: '9d1c250a-e61b-44d9-88ed-5944d1962f5e',
100    // No suffix for production config
101    OAUTH_FILE_SUFFIX: '',
102    MCP_PROXY_URL: 'https://mcp-proxy.anthropic.com',
103    MCP_PROXY_PATH: '/v1/mcp/{server_id}',
104  } as const
105  
106  /**
107   * Client ID Metadata Document URL for MCP OAuth (CIMD / SEP-991).
108   * When an MCP auth server advertises client_id_metadata_document_supported: true,
109   * Claude Code uses this URL as its client_id instead of Dynamic Client Registration.
110   * The URL must point to a JSON document hosted by Anthropic.
111   * See: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-client-id-metadata-document-00
112   */
113  export const MCP_CLIENT_METADATA_URL =
114    'https://claude.ai/oauth/claude-code-client-metadata'
115  
116  // Staging OAuth configuration - only included in ant builds with staging flag
117  // Uses literal check for dead code elimination
118  const STAGING_OAUTH_CONFIG =
119    process.env.USER_TYPE === 'ant'
120      ? ({
121          BASE_API_URL: 'https://api-staging.anthropic.com',
122          CONSOLE_AUTHORIZE_URL:
123            'https://platform.staging.ant.dev/oauth/authorize',
124          CLAUDE_AI_AUTHORIZE_URL:
125            'https://claude-ai.staging.ant.dev/oauth/authorize',
126          CLAUDE_AI_ORIGIN: 'https://claude-ai.staging.ant.dev',
127          TOKEN_URL: 'https://platform.staging.ant.dev/v1/oauth/token',
128          API_KEY_URL:
129            'https://api-staging.anthropic.com/api/oauth/claude_cli/create_api_key',
130          ROLES_URL:
131            'https://api-staging.anthropic.com/api/oauth/claude_cli/roles',
132          CONSOLE_SUCCESS_URL:
133            'https://platform.staging.ant.dev/buy_credits?returnUrl=/oauth/code/success%3Fapp%3Dclaude-code',
134          CLAUDEAI_SUCCESS_URL:
135            'https://platform.staging.ant.dev/oauth/code/success?app=claude-code',
136          MANUAL_REDIRECT_URL:
137            'https://platform.staging.ant.dev/oauth/code/callback',
138          CLIENT_ID: '22422756-60c9-4084-8eb7-27705fd5cf9a',
139          OAUTH_FILE_SUFFIX: '-staging-oauth',
140          MCP_PROXY_URL: 'https://mcp-proxy-staging.anthropic.com',
141          MCP_PROXY_PATH: '/v1/mcp/{server_id}',
142        } as const)
143      : undefined
144  
145  // Three local dev servers: :8000 api-proxy (`api dev start -g ccr`),
146  // :4000 claude-ai frontend, :3000 Console frontend. Env vars let
147  // scripts/claude-localhost override if your layout differs.
148  function getLocalOauthConfig(): OauthConfig {
149    const api =
150      process.env.CLAUDE_LOCAL_OAUTH_API_BASE?.replace(/\/$/, '') ??
151      'http://localhost:8000'
152    const apps =
153      process.env.CLAUDE_LOCAL_OAUTH_APPS_BASE?.replace(/\/$/, '') ??
154      'http://localhost:4000'
155    const consoleBase =
156      process.env.CLAUDE_LOCAL_OAUTH_CONSOLE_BASE?.replace(/\/$/, '') ??
157      'http://localhost:3000'
158    return {
159      BASE_API_URL: api,
160      CONSOLE_AUTHORIZE_URL: `${consoleBase}/oauth/authorize`,
161      CLAUDE_AI_AUTHORIZE_URL: `${apps}/oauth/authorize`,
162      CLAUDE_AI_ORIGIN: apps,
163      TOKEN_URL: `${api}/v1/oauth/token`,
164      API_KEY_URL: `${api}/api/oauth/claude_cli/create_api_key`,
165      ROLES_URL: `${api}/api/oauth/claude_cli/roles`,
166      CONSOLE_SUCCESS_URL: `${consoleBase}/buy_credits?returnUrl=/oauth/code/success%3Fapp%3Dclaude-code`,
167      CLAUDEAI_SUCCESS_URL: `${consoleBase}/oauth/code/success?app=claude-code`,
168      MANUAL_REDIRECT_URL: `${consoleBase}/oauth/code/callback`,
169      CLIENT_ID: '22422756-60c9-4084-8eb7-27705fd5cf9a',
170      OAUTH_FILE_SUFFIX: '-local-oauth',
171      MCP_PROXY_URL: 'http://localhost:8205',
172      MCP_PROXY_PATH: '/v1/toolbox/shttp/mcp/{server_id}',
173    }
174  }
175  
176  // Allowed base URLs for CLAUDE_CODE_CUSTOM_OAUTH_URL override.
177  // Only FedStart/PubSec deployments are permitted to prevent OAuth tokens
178  // from being sent to arbitrary endpoints.
179  const ALLOWED_OAUTH_BASE_URLS = [
180    'https://beacon.claude-ai.staging.ant.dev',
181    'https://claude.fedstart.com',
182    'https://claude-staging.fedstart.com',
183  ]
184  
185  // Default to prod config, override with test/staging if enabled
186  export function getOauthConfig(): OauthConfig {
187    let config: OauthConfig = (() => {
188      switch (getOauthConfigType()) {
189        case 'local':
190          return getLocalOauthConfig()
191        case 'staging':
192          return STAGING_OAUTH_CONFIG ?? PROD_OAUTH_CONFIG
193        case 'prod':
194          return PROD_OAUTH_CONFIG
195      }
196    })()
197  
198    // Allow overriding all OAuth URLs to point to an approved FedStart deployment.
199    // Only allowlisted base URLs are accepted to prevent credential leakage.
200    const oauthBaseUrl = process.env.CLAUDE_CODE_CUSTOM_OAUTH_URL
201    if (oauthBaseUrl) {
202      const base = oauthBaseUrl.replace(/\/$/, '')
203      if (!ALLOWED_OAUTH_BASE_URLS.includes(base)) {
204        throw new Error(
205          'CLAUDE_CODE_CUSTOM_OAUTH_URL is not an approved endpoint.',
206        )
207      }
208      config = {
209        ...config,
210        BASE_API_URL: base,
211        CONSOLE_AUTHORIZE_URL: `${base}/oauth/authorize`,
212        CLAUDE_AI_AUTHORIZE_URL: `${base}/oauth/authorize`,
213        CLAUDE_AI_ORIGIN: base,
214        TOKEN_URL: `${base}/v1/oauth/token`,
215        API_KEY_URL: `${base}/api/oauth/claude_cli/create_api_key`,
216        ROLES_URL: `${base}/api/oauth/claude_cli/roles`,
217        CONSOLE_SUCCESS_URL: `${base}/oauth/code/success?app=claude-code`,
218        CLAUDEAI_SUCCESS_URL: `${base}/oauth/code/success?app=claude-code`,
219        MANUAL_REDIRECT_URL: `${base}/oauth/code/callback`,
220        OAUTH_FILE_SUFFIX: '-custom-oauth',
221      }
222    }
223  
224    // Allow CLIENT_ID override via environment variable (e.g., for Xcode integration)
225    const clientIdOverride = process.env.CLAUDE_CODE_OAUTH_CLIENT_ID
226    if (clientIdOverride) {
227      config = {
228        ...config,
229        CLIENT_ID: clientIdOverride,
230      }
231    }
232  
233    return config
234  }