api.ts
commands/remote-setup/api.ts
183
Lines
5519
Bytes
7
Exports
5
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file lives in the command layer. It likely turns a user action into concrete program behavior.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with commands, integrations, remote-bridge. It contains 183 lines, 5 detected imports, and 7 detected exports.
Important relationships
Detected exports
RedactedGithubTokenImportTokenResultImportTokenErrorimportGithubTokencreateDefaultEnvironmentisSignedIngetCodeWebUrl
Keywords
tokenkindresponsestatusaccesstokenorguuidcatchaxiosredactedcode
Detected imports
axios../../constants/oauth.js../../utils/debug.js../../utils/teleport/api.js../../utils/teleport/environments.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
import axios from 'axios'
import { getOauthConfig } from '../../constants/oauth.js'
import { logForDebugging } from '../../utils/debug.js'
import { getOAuthHeaders, prepareApiRequest } from '../../utils/teleport/api.js'
import { fetchEnvironments } from '../../utils/teleport/environments.js'
const CCR_BYOC_BETA_HEADER = 'ccr-byoc-2025-07-29'
/**
* Wraps a raw GitHub token so that its string representation is redacted.
* `String(token)`, template literals, `JSON.stringify(token)`, and any
* attached error messages will show `[REDACTED:gh-token]` instead of the
* token value. Call `.reveal()` only at the single point where the raw
* value is placed into an HTTP body.
*/
export class RedactedGithubToken {
readonly #value: string
constructor(raw: string) {
this.#value = raw
}
reveal(): string {
return this.#value
}
toString(): string {
return '[REDACTED:gh-token]'
}
toJSON(): string {
return '[REDACTED:gh-token]'
}
[Symbol.for('nodejs.util.inspect.custom')](): string {
return '[REDACTED:gh-token]'
}
}
export type ImportTokenResult = {
github_username: string
}
export type ImportTokenError =
| { kind: 'not_signed_in' }
| { kind: 'invalid_token' }
| { kind: 'server'; status: number }
| { kind: 'network' }
/**
* POSTs a GitHub token to the CCR backend, which validates it against
* GitHub's /user endpoint and stores it Fernet-encrypted in sync_user_tokens.
* The stored token satisfies the same read paths as an OAuth token, so
* clone/push in claude.ai/code works immediately after this succeeds.
*/
export async function importGithubToken(
token: RedactedGithubToken,
): Promise<
| { ok: true; result: ImportTokenResult }
| { ok: false; error: ImportTokenError }
> {
let accessToken: string, orgUUID: string
try {
;({ accessToken, orgUUID } = await prepareApiRequest())
} catch {
return { ok: false, error: { kind: 'not_signed_in' } }
}
const url = `${getOauthConfig().BASE_API_URL}/v1/code/github/import-token`
const headers = {
...getOAuthHeaders(accessToken),
'anthropic-beta': CCR_BYOC_BETA_HEADER,
'x-organization-uuid': orgUUID,
}
try {
const response = await axios.post<ImportTokenResult>(
url,
{ token: token.reveal() },
{ headers, timeout: 15000, validateStatus: () => true },
)
if (response.status === 200) {
return { ok: true, result: response.data }
}
if (response.status === 400) {
return { ok: false, error: { kind: 'invalid_token' } }
}
if (response.status === 401) {
return { ok: false, error: { kind: 'not_signed_in' } }
}
logForDebugging(`import-token returned ${response.status}`, {
level: 'error',
})
return { ok: false, error: { kind: 'server', status: response.status } }
} catch (err) {
if (axios.isAxiosError(err)) {
// err.config.data would contain the POST body with the raw token.
// Do not include it in any log. The error code alone is enough.
logForDebugging(`import-token network error: ${err.code ?? 'unknown'}`, {
level: 'error',
})
}
return { ok: false, error: { kind: 'network' } }
}
}
async function hasExistingEnvironment(): Promise<boolean> {
try {
const envs = await fetchEnvironments()
return envs.length > 0
} catch {
return false
}
}
/**
* Best-effort default environment creation. Mirrors the web onboarding's
* DEFAULT_CLOUD_ENVIRONMENT_REQUEST so a first-time user lands on the
* composer instead of env-setup. Checks for existing environments first
* so re-running /web-setup doesn't pile up duplicates. Failures are
* non-fatal — the token import already succeeded, and the web state
* machine falls back to env-setup on next load.
*/
export async function createDefaultEnvironment(): Promise<boolean> {
let accessToken: string, orgUUID: string
try {
;({ accessToken, orgUUID } = await prepareApiRequest())
} catch {
return false
}
if (await hasExistingEnvironment()) {
return true
}
// The /private/organizations/{org}/ path rejects CLI OAuth tokens (wrong
// auth dep). The public path uses build_flexible_auth — same path
// fetchEnvironments() uses. Org is passed via x-organization-uuid header.
const url = `${getOauthConfig().BASE_API_URL}/v1/environment_providers/cloud/create`
const headers = {
...getOAuthHeaders(accessToken),
'x-organization-uuid': orgUUID,
}
try {
const response = await axios.post(
url,
{
name: 'Default',
kind: 'anthropic_cloud',
description: 'Default - trusted network access',
config: {
environment_type: 'anthropic',
cwd: '/home/user',
init_script: null,
environment: {},
languages: [
{ name: 'python', version: '3.11' },
{ name: 'node', version: '20' },
],
network_config: {
allowed_hosts: [],
allow_default_hosts: true,
},
},
},
{ headers, timeout: 15000, validateStatus: () => true },
)
return response.status >= 200 && response.status < 300
} catch {
return false
}
}
/** Returns true when the user has valid Claude OAuth credentials. */
export async function isSignedIn(): Promise<boolean> {
try {
await prepareApiRequest()
return true
} catch {
return false
}
}
export function getCodeWebUrl(): string {
return `${getOauthConfig().CLAUDE_AI_ORIGIN}/code`
}