source dump of claude code
at main 182 lines 5.5 kB view raw
1import axios from 'axios' 2import { getOauthConfig } from '../../constants/oauth.js' 3import { logForDebugging } from '../../utils/debug.js' 4import { getOAuthHeaders, prepareApiRequest } from '../../utils/teleport/api.js' 5import { fetchEnvironments } from '../../utils/teleport/environments.js' 6 7const CCR_BYOC_BETA_HEADER = 'ccr-byoc-2025-07-29' 8 9/** 10 * Wraps a raw GitHub token so that its string representation is redacted. 11 * `String(token)`, template literals, `JSON.stringify(token)`, and any 12 * attached error messages will show `[REDACTED:gh-token]` instead of the 13 * token value. Call `.reveal()` only at the single point where the raw 14 * value is placed into an HTTP body. 15 */ 16export class RedactedGithubToken { 17 readonly #value: string 18 constructor(raw: string) { 19 this.#value = raw 20 } 21 reveal(): string { 22 return this.#value 23 } 24 toString(): string { 25 return '[REDACTED:gh-token]' 26 } 27 toJSON(): string { 28 return '[REDACTED:gh-token]' 29 } 30 [Symbol.for('nodejs.util.inspect.custom')](): string { 31 return '[REDACTED:gh-token]' 32 } 33} 34 35export type ImportTokenResult = { 36 github_username: string 37} 38 39export type ImportTokenError = 40 | { kind: 'not_signed_in' } 41 | { kind: 'invalid_token' } 42 | { kind: 'server'; status: number } 43 | { kind: 'network' } 44 45/** 46 * POSTs a GitHub token to the CCR backend, which validates it against 47 * GitHub's /user endpoint and stores it Fernet-encrypted in sync_user_tokens. 48 * The stored token satisfies the same read paths as an OAuth token, so 49 * clone/push in claude.ai/code works immediately after this succeeds. 50 */ 51export async function importGithubToken( 52 token: RedactedGithubToken, 53): Promise< 54 | { ok: true; result: ImportTokenResult } 55 | { ok: false; error: ImportTokenError } 56> { 57 let accessToken: string, orgUUID: string 58 try { 59 ;({ accessToken, orgUUID } = await prepareApiRequest()) 60 } catch { 61 return { ok: false, error: { kind: 'not_signed_in' } } 62 } 63 64 const url = `${getOauthConfig().BASE_API_URL}/v1/code/github/import-token` 65 const headers = { 66 ...getOAuthHeaders(accessToken), 67 'anthropic-beta': CCR_BYOC_BETA_HEADER, 68 'x-organization-uuid': orgUUID, 69 } 70 71 try { 72 const response = await axios.post<ImportTokenResult>( 73 url, 74 { token: token.reveal() }, 75 { headers, timeout: 15000, validateStatus: () => true }, 76 ) 77 if (response.status === 200) { 78 return { ok: true, result: response.data } 79 } 80 if (response.status === 400) { 81 return { ok: false, error: { kind: 'invalid_token' } } 82 } 83 if (response.status === 401) { 84 return { ok: false, error: { kind: 'not_signed_in' } } 85 } 86 logForDebugging(`import-token returned ${response.status}`, { 87 level: 'error', 88 }) 89 return { ok: false, error: { kind: 'server', status: response.status } } 90 } catch (err) { 91 if (axios.isAxiosError(err)) { 92 // err.config.data would contain the POST body with the raw token. 93 // Do not include it in any log. The error code alone is enough. 94 logForDebugging(`import-token network error: ${err.code ?? 'unknown'}`, { 95 level: 'error', 96 }) 97 } 98 return { ok: false, error: { kind: 'network' } } 99 } 100} 101 102async function hasExistingEnvironment(): Promise<boolean> { 103 try { 104 const envs = await fetchEnvironments() 105 return envs.length > 0 106 } catch { 107 return false 108 } 109} 110 111/** 112 * Best-effort default environment creation. Mirrors the web onboarding's 113 * DEFAULT_CLOUD_ENVIRONMENT_REQUEST so a first-time user lands on the 114 * composer instead of env-setup. Checks for existing environments first 115 * so re-running /web-setup doesn't pile up duplicates. Failures are 116 * non-fatal — the token import already succeeded, and the web state 117 * machine falls back to env-setup on next load. 118 */ 119export async function createDefaultEnvironment(): Promise<boolean> { 120 let accessToken: string, orgUUID: string 121 try { 122 ;({ accessToken, orgUUID } = await prepareApiRequest()) 123 } catch { 124 return false 125 } 126 127 if (await hasExistingEnvironment()) { 128 return true 129 } 130 131 // The /private/organizations/{org}/ path rejects CLI OAuth tokens (wrong 132 // auth dep). The public path uses build_flexible_auth — same path 133 // fetchEnvironments() uses. Org is passed via x-organization-uuid header. 134 const url = `${getOauthConfig().BASE_API_URL}/v1/environment_providers/cloud/create` 135 const headers = { 136 ...getOAuthHeaders(accessToken), 137 'x-organization-uuid': orgUUID, 138 } 139 140 try { 141 const response = await axios.post( 142 url, 143 { 144 name: 'Default', 145 kind: 'anthropic_cloud', 146 description: 'Default - trusted network access', 147 config: { 148 environment_type: 'anthropic', 149 cwd: '/home/user', 150 init_script: null, 151 environment: {}, 152 languages: [ 153 { name: 'python', version: '3.11' }, 154 { name: 'node', version: '20' }, 155 ], 156 network_config: { 157 allowed_hosts: [], 158 allow_default_hosts: true, 159 }, 160 }, 161 }, 162 { headers, timeout: 15000, validateStatus: () => true }, 163 ) 164 return response.status >= 200 && response.status < 300 165 } catch { 166 return false 167 } 168} 169 170/** Returns true when the user has valid Claude OAuth credentials. */ 171export async function isSignedIn(): Promise<boolean> { 172 try { 173 await prepareApiRequest() 174 return true 175 } catch { 176 return false 177 } 178} 179 180export function getCodeWebUrl(): string { 181 return `${getOauthConfig().CLAUDE_AI_ORIGIN}/code` 182}