/ utils / teleport / environments.ts
environments.ts
  1  import axios from 'axios'
  2  import { getOauthConfig } from 'src/constants/oauth.js'
  3  import { getOrganizationUUID } from 'src/services/oauth/client.js'
  4  import { getClaudeAIOAuthTokens } from '../auth.js'
  5  import { toError } from '../errors.js'
  6  import { logError } from '../log.js'
  7  import { getOAuthHeaders } from './api.js'
  8  
  9  export type EnvironmentKind = 'anthropic_cloud' | 'byoc' | 'bridge'
 10  export type EnvironmentState = 'active'
 11  
 12  export type EnvironmentResource = {
 13    kind: EnvironmentKind
 14    environment_id: string
 15    name: string
 16    created_at: string
 17    state: EnvironmentState
 18  }
 19  
 20  export type EnvironmentListResponse = {
 21    environments: EnvironmentResource[]
 22    has_more: boolean
 23    first_id: string | null
 24    last_id: string | null
 25  }
 26  
 27  /**
 28   * Fetches the list of available environments from the Environment API
 29   * @returns Promise<EnvironmentResource[]> Array of available environments
 30   * @throws Error if the API request fails or no access token is available
 31   */
 32  export async function fetchEnvironments(): Promise<EnvironmentResource[]> {
 33    const accessToken = getClaudeAIOAuthTokens()?.accessToken
 34    if (!accessToken) {
 35      throw new Error(
 36        'Claude Code web sessions require authentication with a Claude.ai account. API key authentication is not sufficient. Please run /login to authenticate, or check your authentication status with /status.',
 37      )
 38    }
 39  
 40    const orgUUID = await getOrganizationUUID()
 41    if (!orgUUID) {
 42      throw new Error('Unable to get organization UUID')
 43    }
 44  
 45    const url = `${getOauthConfig().BASE_API_URL}/v1/environment_providers`
 46  
 47    try {
 48      const headers = {
 49        ...getOAuthHeaders(accessToken),
 50        'x-organization-uuid': orgUUID,
 51      }
 52  
 53      const response = await axios.get<EnvironmentListResponse>(url, {
 54        headers,
 55        timeout: 15000,
 56      })
 57  
 58      if (response.status !== 200) {
 59        throw new Error(
 60          `Failed to fetch environments: ${response.status} ${response.statusText}`,
 61        )
 62      }
 63  
 64      return response.data.environments
 65    } catch (error) {
 66      const err = toError(error)
 67      logError(err)
 68      throw new Error(`Failed to fetch environments: ${err.message}`)
 69    }
 70  }
 71  
 72  /**
 73   * Creates a default anthropic_cloud environment for users who have none.
 74   * Uses the public environment_providers route (same auth as fetchEnvironments).
 75   */
 76  export async function createDefaultCloudEnvironment(
 77    name: string,
 78  ): Promise<EnvironmentResource> {
 79    const accessToken = getClaudeAIOAuthTokens()?.accessToken
 80    if (!accessToken) {
 81      throw new Error('No access token available')
 82    }
 83    const orgUUID = await getOrganizationUUID()
 84    if (!orgUUID) {
 85      throw new Error('Unable to get organization UUID')
 86    }
 87  
 88    const url = `${getOauthConfig().BASE_API_URL}/v1/environment_providers/cloud/create`
 89    const response = await axios.post<EnvironmentResource>(
 90      url,
 91      {
 92        name,
 93        kind: 'anthropic_cloud',
 94        description: '',
 95        config: {
 96          environment_type: 'anthropic',
 97          cwd: '/home/user',
 98          init_script: null,
 99          environment: {},
100          languages: [
101            { name: 'python', version: '3.11' },
102            { name: 'node', version: '20' },
103          ],
104          network_config: {
105            allowed_hosts: [],
106            allow_default_hosts: true,
107          },
108        },
109      },
110      {
111        headers: {
112          ...getOAuthHeaders(accessToken),
113          'anthropic-beta': 'ccr-byoc-2025-07-29',
114          'x-organization-uuid': orgUUID,
115        },
116        timeout: 15000,
117      },
118    )
119    return response.data
120  }