Download src/services/oauth/client.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 18.3 kB
-
https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/services/oauth/client.ts
- Command line
-
hf download hf://chenbhao/codev@afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/services/oauth/client.ts
-
curl -L -o client.ts https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/services/oauth/client.ts
18.3 kB
| // OAuth client for handling authentication flows with Claude services | |
| import axios from 'axios' | |
| import { | |
| type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| logEvent, | |
| } from 'src/services/analytics/index.js' | |
| import { | |
| ALL_OAUTH_SCOPES, | |
| CLAUDE_AI_INFERENCE_SCOPE, | |
| CLAUDE_AI_OAUTH_SCOPES, | |
| getOauthConfig, | |
| } from '../../constants/oauth.js' | |
| import { | |
| checkAndRefreshOAuthTokenIfNeeded, | |
| getClaudeAIOAuthTokens, | |
| hasProfileScope, | |
| isClaudeAISubscriber, | |
| saveApiKey, | |
| } from '../../utils/auth.js' | |
| import type { AccountInfo } from '../../utils/config.js' | |
| import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js' | |
| import { logForDebugging } from '../../utils/debug.js' | |
| import { getOauthProfileFromOauthToken } from './getOauthProfile.js' | |
| import type { | |
| BillingType, | |
| OAuthProfileResponse, | |
| OAuthTokenExchangeResponse, | |
| OAuthTokens, | |
| RateLimitTier, | |
| SubscriptionType, | |
| UserRolesResponse, | |
| } from './types.js' | |
| /** | |
| * Check if the user has Claude.ai authentication scope | |
| * @private Only call this if you're OAuth / auth related code! | |
| */ | |
| export function shouldUseClaudeAIAuth(scopes: string[] | undefined): boolean { | |
| return Boolean(scopes?.includes(CLAUDE_AI_INFERENCE_SCOPE)) | |
| } | |
| export function parseScopes(scopeString?: string): string[] { | |
| return scopeString?.split(' ').filter(Boolean) ?? [] | |
| } | |
| export function buildAuthUrl({ | |
| codeChallenge, | |
| state, | |
| port, | |
| isManual, | |
| loginWithClaudeAi, | |
| inferenceOnly, | |
| orgUUID, | |
| loginHint, | |
| loginMethod, | |
| }: { | |
| codeChallenge: string | |
| state: string | |
| port: number | |
| isManual: boolean | |
| loginWithClaudeAi?: boolean | |
| inferenceOnly?: boolean | |
| orgUUID?: string | |
| loginHint?: string | |
| loginMethod?: string | |
| }): string { | |
| const authUrlBase = loginWithClaudeAi | |
| ? getOauthConfig().CLAUDE_AI_AUTHORIZE_URL | |
| : getOauthConfig().CONSOLE_AUTHORIZE_URL | |
| const authUrl = new URL(authUrlBase) | |
| authUrl.searchParams.append('code', 'true') // this tells the login page to show Claude Max upsell | |
| authUrl.searchParams.append('client_id', getOauthConfig().CLIENT_ID) | |
| authUrl.searchParams.append('response_type', 'code') | |
| authUrl.searchParams.append( | |
| 'redirect_uri', | |
| isManual | |
| ? getOauthConfig().MANUAL_REDIRECT_URL | |
| : `http://localhost:${port}/callback`, | |
| ) | |
| const scopesToUse = inferenceOnly | |
| ? [CLAUDE_AI_INFERENCE_SCOPE] // Long-lived inference-only tokens | |
| : ALL_OAUTH_SCOPES | |
| authUrl.searchParams.append('scope', scopesToUse.join(' ')) | |
| authUrl.searchParams.append('code_challenge', codeChallenge) | |
| authUrl.searchParams.append('code_challenge_method', 'S256') | |
| authUrl.searchParams.append('state', state) | |
| // Add orgUUID as URL param if provided | |
| if (orgUUID) { | |
| authUrl.searchParams.append('orgUUID', orgUUID) | |
| } | |
| // Pre-populate email on the login form (standard OIDC parameter) | |
| if (loginHint) { | |
| authUrl.searchParams.append('login_hint', loginHint) | |
| } | |
| // Request a specific login method (e.g. 'sso', 'magic_link', 'google') | |
| if (loginMethod) { | |
| authUrl.searchParams.append('login_method', loginMethod) | |
| } | |
| return authUrl.toString() | |
| } | |
| export async function exchangeCodeForTokens( | |
| authorizationCode: string, | |
| state: string, | |
| codeVerifier: string, | |
| port: number, | |
| useManualRedirect: boolean = false, | |
| expiresIn?: number, | |
| ): Promise<OAuthTokenExchangeResponse> { | |
| const requestBody: Record<string, string | number> = { | |
| grant_type: 'authorization_code', | |
| code: authorizationCode, | |
| redirect_uri: useManualRedirect | |
| ? getOauthConfig().MANUAL_REDIRECT_URL | |
| : `http://localhost:${port}/callback`, | |
| client_id: getOauthConfig().CLIENT_ID, | |
| code_verifier: codeVerifier, | |
| state, | |
| } | |
| if (expiresIn !== undefined) { | |
| requestBody.expires_in = expiresIn | |
| } | |
| const response = await axios.post(getOauthConfig().TOKEN_URL, requestBody, { | |
| headers: { 'Content-Type': 'application/json' }, | |
| timeout: 15000, | |
| }) | |
| if (response.status !== 200) { | |
| throw new Error( | |
| response.status === 401 | |
| ? 'Authentication failed: Invalid authorization code' | |
| : `Token exchange failed (${response.status}): ${response.statusText}`, | |
| ) | |
| } | |
| logEvent('tengu_oauth_token_exchange_success', {}) | |
| return response.data | |
| } | |
| export async function refreshOAuthToken( | |
| refreshToken: string, | |
| { scopes: requestedScopes }: { scopes?: string[] } = {}, | |
| ): Promise<OAuthTokens> { | |
| const requestBody = { | |
| grant_type: 'refresh_token', | |
| refresh_token: refreshToken, | |
| client_id: getOauthConfig().CLIENT_ID, | |
| // Request specific scopes, defaulting to the full Claude AI set. The | |
| // backend's refresh-token grant allows scope expansion beyond what the | |
| // initial authorize granted (see ALLOWED_SCOPE_EXPANSIONS), so this is | |
| // safe even for tokens issued before scopes were added to the app's | |
| // registered oauth_scope. | |
| scope: (requestedScopes?.length | |
| ? requestedScopes | |
| : CLAUDE_AI_OAUTH_SCOPES | |
| ).join(' '), | |
| } | |
| try { | |
| const response = await axios.post(getOauthConfig().TOKEN_URL, requestBody, { | |
| headers: { 'Content-Type': 'application/json' }, | |
| timeout: 15000, | |
| }) | |
| if (response.status !== 200) { | |
| throw new Error(`Token refresh failed: ${response.statusText}`) | |
| } | |
| const data = response.data as OAuthTokenExchangeResponse | |
| const { | |
| access_token: accessToken, | |
| refresh_token: newRefreshToken = refreshToken, | |
| expires_in: expiresIn, | |
| } = data | |
| const expiresAt = Date.now() + expiresIn * 1000 | |
| const scopes = parseScopes(data.scope) | |
| logEvent('tengu_oauth_token_refresh_success', {}) | |
| // Skip the extra /api/oauth/profile round-trip when we already have both | |
| // the global-config profile fields AND the secure-storage subscription data. | |
| // Routine refreshes satisfy both, so we cut ~7M req/day fleet-wide. | |
| // | |
| // Checking secure storage (not just config) matters for the | |
| // CLAUDE_CODE_OAUTH_REFRESH_TOKEN re-login path: installOAuthTokens runs | |
| // performLogout() AFTER we return, wiping secure storage. If we returned | |
| // null for subscriptionType here, saveOAuthTokensIfNeeded would persist | |
| // null ?? (wiped) ?? null = null, and every future refresh would see the | |
| // config guard fields satisfied and skip again, permanently losing the | |
| // subscription type for paying users. By passing through existing values, | |
| // the re-login path writes cached ?? wiped ?? null = cached; and if secure | |
| // storage was already empty we fall through to the fetch. | |
| const config = getGlobalConfig() | |
| const existing = getClaudeAIOAuthTokens() | |
| const haveProfileAlready = | |
| config.oauthAccount?.billingType !== undefined && | |
| config.oauthAccount?.accountCreatedAt !== undefined && | |
| config.oauthAccount?.subscriptionCreatedAt !== undefined && | |
| existing?.subscriptionType != null && | |
| existing?.rateLimitTier != null | |
| const profileInfo = haveProfileAlready | |
| ? null | |
| : await fetchProfileInfo(accessToken) | |
| // Update the stored properties if they have changed | |
| if (profileInfo && config.oauthAccount) { | |
| const updates: Partial<AccountInfo> = {} | |
| if (profileInfo.displayName !== undefined) { | |
| updates.displayName = profileInfo.displayName | |
| } | |
| if (typeof profileInfo.hasExtraUsageEnabled === 'boolean') { | |
| updates.hasExtraUsageEnabled = profileInfo.hasExtraUsageEnabled | |
| } | |
| if (profileInfo.billingType !== null) { | |
| updates.billingType = profileInfo.billingType | |
| } | |
| if (profileInfo.accountCreatedAt !== undefined) { | |
| updates.accountCreatedAt = profileInfo.accountCreatedAt | |
| } | |
| if (profileInfo.subscriptionCreatedAt !== undefined) { | |
| updates.subscriptionCreatedAt = profileInfo.subscriptionCreatedAt | |
| } | |
| if (Object.keys(updates).length > 0) { | |
| saveGlobalConfig(current => ({ | |
| ...current, | |
| oauthAccount: current.oauthAccount | |
| ? { ...current.oauthAccount, ...updates } | |
| : current.oauthAccount, | |
| })) | |
| } | |
| } | |
| return { | |
| accessToken, | |
| refreshToken: newRefreshToken, | |
| expiresAt, | |
| scopes, | |
| subscriptionType: | |
| profileInfo?.subscriptionType ?? existing?.subscriptionType ?? null, | |
| rateLimitTier: | |
| profileInfo?.rateLimitTier ?? existing?.rateLimitTier ?? null, | |
| profile: profileInfo?.rawProfile, | |
| tokenAccount: data.account | |
| ? { | |
| uuid: data.account.uuid, | |
| emailAddress: data.account.email_address, | |
| organizationUuid: data.organization?.uuid, | |
| } | |
| : undefined, | |
| } | |
| } catch (error) { | |
| const responseBody = | |
| axios.isAxiosError(error) && error.response?.data | |
| ? JSON.stringify(error.response.data) | |
| : undefined | |
| logEvent('tengu_oauth_token_refresh_failure', { | |
| error: (error as Error) | |
| .message as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| ...(responseBody && { | |
| responseBody: | |
| responseBody as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| }), | |
| }) | |
| throw error | |
| } | |
| } | |
| export async function fetchAndStoreUserRoles( | |
| accessToken: string, | |
| ): Promise<void> { | |
| const response = await axios.get(getOauthConfig().ROLES_URL, { | |
| headers: { Authorization: `Bearer ${accessToken}` }, | |
| }) | |
| if (response.status !== 200) { | |
| throw new Error(`Failed to fetch user roles: ${response.statusText}`) | |
| } | |
| const data = response.data as UserRolesResponse | |
| const config = getGlobalConfig() | |
| if (!config.oauthAccount) { | |
| throw new Error('OAuth account information not found in config') | |
| } | |
| saveGlobalConfig(current => ({ | |
| ...current, | |
| oauthAccount: current.oauthAccount | |
| ? { | |
| ...current.oauthAccount, | |
| organizationRole: data.organization_role, | |
| workspaceRole: data.workspace_role, | |
| organizationName: data.organization_name, | |
| } | |
| : current.oauthAccount, | |
| })) | |
| logEvent('tengu_oauth_roles_stored', { | |
| org_role: | |
| data.organization_role as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| }) | |
| } | |
| export async function createAndStoreApiKey( | |
| accessToken: string, | |
| ): Promise<string | null> { | |
| try { | |
| const response = await axios.post(getOauthConfig().API_KEY_URL, null, { | |
| headers: { Authorization: `Bearer ${accessToken}` }, | |
| }) | |
| const apiKey = response.data?.raw_key | |
| if (apiKey) { | |
| await saveApiKey(apiKey) | |
| logEvent('tengu_oauth_api_key', { | |
| status: | |
| 'success' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| statusCode: response.status, | |
| }) | |
| return apiKey | |
| } | |
| return null | |
| } catch (error) { | |
| logEvent('tengu_oauth_api_key', { | |
| status: | |
| 'failure' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| error: (error instanceof Error | |
| ? error.message | |
| : String( | |
| error, | |
| )) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| }) | |
| throw error | |
| } | |
| } | |
| export function isOAuthTokenExpired(expiresAt: number | null): boolean { | |
| if (expiresAt === null) { | |
| return false | |
| } | |
| const bufferTime = 5 * 60 * 1000 | |
| const now = Date.now() | |
| const expiresWithBuffer = now + bufferTime | |
| return expiresWithBuffer >= expiresAt | |
| } | |
| export async function fetchProfileInfo(accessToken: string): Promise<{ | |
| subscriptionType: SubscriptionType | null | |
| displayName?: string | |
| rateLimitTier: RateLimitTier | null | |
| hasExtraUsageEnabled: boolean | null | |
| billingType: BillingType | null | |
| accountCreatedAt?: string | |
| subscriptionCreatedAt?: string | |
| rawProfile?: OAuthProfileResponse | |
| }> { | |
| const profile = await getOauthProfileFromOauthToken(accessToken) | |
| const orgType = profile?.organization?.organization_type | |
| // Reuse the logic from fetchSubscriptionType | |
| let subscriptionType: SubscriptionType | null = null | |
| switch (orgType) { | |
| case 'claude_max': | |
| subscriptionType = 'max' | |
| break | |
| case 'claude_pro': | |
| subscriptionType = 'pro' | |
| break | |
| case 'claude_enterprise': | |
| subscriptionType = 'enterprise' | |
| break | |
| case 'claude_team': | |
| subscriptionType = 'team' | |
| break | |
| default: | |
| // Return null for unknown organization types | |
| subscriptionType = null | |
| break | |
| } | |
| const result: { | |
| subscriptionType: SubscriptionType | null | |
| displayName?: string | |
| rateLimitTier: RateLimitTier | null | |
| hasExtraUsageEnabled: boolean | null | |
| billingType: BillingType | null | |
| accountCreatedAt?: string | |
| subscriptionCreatedAt?: string | |
| } = { | |
| subscriptionType, | |
| rateLimitTier: profile?.organization?.rate_limit_tier ?? null, | |
| hasExtraUsageEnabled: | |
| profile?.organization?.has_extra_usage_enabled ?? null, | |
| billingType: profile?.organization?.billing_type ?? null, | |
| } | |
| if (profile?.account?.display_name) { | |
| result.displayName = profile.account.display_name | |
| } | |
| if (profile?.account?.created_at) { | |
| result.accountCreatedAt = profile.account.created_at | |
| } | |
| if (profile?.organization?.subscription_created_at) { | |
| result.subscriptionCreatedAt = profile.organization.subscription_created_at | |
| } | |
| logEvent('tengu_oauth_profile_fetch_success', {}) | |
| return { ...result, rawProfile: profile } | |
| } | |
| /** | |
| * Gets the organization UUID from the OAuth access token | |
| * @returns The organization UUID or null if not authenticated | |
| */ | |
| export async function getOrganizationUUID(): Promise<string | null> { | |
| // Check global config first to avoid unnecessary API call | |
| const globalConfig = getGlobalConfig() | |
| const orgUUID = globalConfig.oauthAccount?.organizationUuid | |
| if (orgUUID) { | |
| return orgUUID | |
| } | |
| // Fall back to fetching from profile (requires user:profile scope) | |
| const accessToken = getClaudeAIOAuthTokens()?.accessToken | |
| if (accessToken === undefined || !hasProfileScope()) { | |
| return null | |
| } | |
| const profile = await getOauthProfileFromOauthToken(accessToken) | |
| const profileOrgUUID = profile?.organization?.uuid | |
| if (!profileOrgUUID) { | |
| return null | |
| } | |
| return profileOrgUUID | |
| } | |
| /** | |
| * Populate the OAuth account info if it has not already been cached in config. | |
| * @returns Whether or not the oauth account info was populated. | |
| */ | |
| export async function populateOAuthAccountInfoIfNeeded(): Promise<boolean> { | |
| // Check env vars first (synchronous, no network call needed). | |
| // SDK callers like Cowork can provide account info directly, which also | |
| // eliminates the race condition where early telemetry events lack account info. | |
| // NB: If/when adding additional SDK-relevant functionality requiring _other_ OAuth account properties, | |
| // please reach out to #proj-cowork so the team can add additional env var fallbacks. | |
| const envAccountUuid = process.env.CLAUDE_CODE_ACCOUNT_UUID | |
| const envUserEmail = process.env.CLAUDE_CODE_USER_EMAIL | |
| const envOrganizationUuid = process.env.CLAUDE_CODE_ORGANIZATION_UUID | |
| const hasEnvVars = Boolean( | |
| envAccountUuid && envUserEmail && envOrganizationUuid, | |
| ) | |
| if (envAccountUuid && envUserEmail && envOrganizationUuid) { | |
| if (!getGlobalConfig().oauthAccount) { | |
| storeOAuthAccountInfo({ | |
| accountUuid: envAccountUuid, | |
| emailAddress: envUserEmail, | |
| organizationUuid: envOrganizationUuid, | |
| }) | |
| } | |
| } | |
| // Wait for any in-flight token refresh to complete first, since | |
| // refreshOAuthToken already fetches and stores profile info | |
| await checkAndRefreshOAuthTokenIfNeeded() | |
| const config = getGlobalConfig() | |
| if ( | |
| (config.oauthAccount && | |
| config.oauthAccount.billingType !== undefined && | |
| config.oauthAccount.accountCreatedAt !== undefined && | |
| config.oauthAccount.subscriptionCreatedAt !== undefined) || | |
| !isClaudeAISubscriber() || | |
| !hasProfileScope() | |
| ) { | |
| return false | |
| } | |
| const tokens = getClaudeAIOAuthTokens() | |
| if (tokens?.accessToken) { | |
| const profile = await getOauthProfileFromOauthToken(tokens.accessToken) | |
| if (profile) { | |
| if (hasEnvVars) { | |
| logForDebugging( | |
| 'OAuth profile fetch succeeded, overriding env var account info', | |
| { level: 'info' }, | |
| ) | |
| } | |
| storeOAuthAccountInfo({ | |
| accountUuid: profile.account.uuid, | |
| emailAddress: profile.account.email, | |
| organizationUuid: profile.organization.uuid, | |
| displayName: profile.account.display_name || undefined, | |
| hasExtraUsageEnabled: | |
| profile.organization.has_extra_usage_enabled ?? false, | |
| billingType: profile.organization.billing_type ?? undefined, | |
| accountCreatedAt: profile.account.created_at, | |
| subscriptionCreatedAt: | |
| profile.organization.subscription_created_at ?? undefined, | |
| }) | |
| return true | |
| } | |
| } | |
| return false | |
| } | |
| export function storeOAuthAccountInfo({ | |
| accountUuid, | |
| emailAddress, | |
| organizationUuid, | |
| displayName, | |
| hasExtraUsageEnabled, | |
| billingType, | |
| accountCreatedAt, | |
| subscriptionCreatedAt, | |
| }: { | |
| accountUuid: string | |
| emailAddress: string | |
| organizationUuid: string | undefined | |
| displayName?: string | |
| hasExtraUsageEnabled?: boolean | |
| billingType?: BillingType | |
| accountCreatedAt?: string | |
| subscriptionCreatedAt?: string | |
| }): void { | |
| const accountInfo: AccountInfo = { | |
| accountUuid, | |
| emailAddress, | |
| organizationUuid, | |
| hasExtraUsageEnabled, | |
| billingType, | |
| accountCreatedAt, | |
| subscriptionCreatedAt, | |
| } | |
| if (displayName) { | |
| accountInfo.displayName = displayName | |
| } | |
| saveGlobalConfig(current => { | |
| // For oauthAccount we need to compare content since it's an object | |
| if ( | |
| current.oauthAccount?.accountUuid === accountInfo.accountUuid && | |
| current.oauthAccount?.emailAddress === accountInfo.emailAddress && | |
| current.oauthAccount?.organizationUuid === accountInfo.organizationUuid && | |
| current.oauthAccount?.displayName === accountInfo.displayName && | |
| current.oauthAccount?.hasExtraUsageEnabled === | |
| accountInfo.hasExtraUsageEnabled && | |
| current.oauthAccount?.billingType === accountInfo.billingType && | |
| current.oauthAccount?.accountCreatedAt === accountInfo.accountCreatedAt && | |
| current.oauthAccount?.subscriptionCreatedAt === | |
| accountInfo.subscriptionCreatedAt | |
| ) { | |
| return current | |
| } | |
| return { ...current, oauthAccount: accountInfo } | |
| }) | |
| } | |