Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
18 kB · 539 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540import axios from 'axios'
import { debugBody, extractErrorDetail } from './debugUtils.js'import { BRIDGE_LOGIN_INSTRUCTION, type BridgeApiClient, type BridgeConfig, type PermissionResponseEvent, type WorkResponse,} from './types.js'
type BridgeApiDeps = { baseUrl: string getAccessToken: () => string | undefined runnerVersion: string onDebug?: (msg: string) => void /** * Called on 401 to attempt OAuth token refresh. Returns true if refreshed, * in which case the request is retried once. Injected because * handleOAuth401Error from utils/auth.ts transitively pulls in config.ts → * file.ts → permissions/filesystem.ts → sessionStorage.ts → commands.ts * (~1300 modules). Daemon callers using env-var tokens omit this — their * tokens don't refresh, so 401 goes straight to BridgeFatalError. */ onAuth401?: (staleAccessToken: string) => Promise<boolean> /** * Returns the trusted device token to send as X-Trusted-Device-Token on * bridge API calls. Bridge sessions have SecurityTier=ELEVATED on the * server (CCR v2); when the server's enforcement flag is on, * ConnectBridgeWorker requires a trusted device at JWT-issuance. * Optional — when absent or returning undefined, the header is omitted * and the server falls through to its flag-off/no-op path. The CLI-side * gate is tengu_sessions_elevated_auth_enforcement (see trustedDevice.ts). */ getTrustedDeviceToken?: () => string | undefined}
const BETA_HEADER = 'environments-2025-11-01'
/** Allowlist pattern for server-provided IDs used in URL path segments. */const SAFE_ID_PATTERN = /^[a-zA-Z0-9_-]+$/
/** * Validate that a server-provided ID is safe to interpolate into a URL path. * Prevents path traversal (e.g. `../../admin`) and injection via IDs that * contain slashes, dots, or other special characters. */export function validateBridgeId(id: string, label: string): string { if (!id || !SAFE_ID_PATTERN.test(id)) { throw new Error(`Invalid ${label}: contains unsafe characters`) } return id}
/** Fatal bridge errors that should not be retried (e.g. auth failures). */export class BridgeFatalError extends Error { readonly status: number /** Server-provided error type, e.g. "environment_expired". */ readonly errorType: string | undefined constructor(message: string, status: number, errorType?: string) { super(message) this.name = 'BridgeFatalError' this.status = status this.errorType = errorType }}
export function createBridgeApiClient(deps: BridgeApiDeps): BridgeApiClient { function debug(msg: string): void { deps.onDebug?.(msg) }
let consecutiveEmptyPolls = 0 const EMPTY_POLL_LOG_INTERVAL = 100
function getHeaders(accessToken: string): Record<string, string> { const headers: Record<string, string> = { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', 'anthropic-version': '2023-06-01', 'anthropic-beta': BETA_HEADER, 'x-environment-runner-version': deps.runnerVersion, } const deviceToken = deps.getTrustedDeviceToken?.() if (deviceToken) { headers['X-Trusted-Device-Token'] = deviceToken } return headers }
function resolveAuth(): string { const accessToken = deps.getAccessToken() if (!accessToken) { throw new Error(BRIDGE_LOGIN_INSTRUCTION) } return accessToken }
/** * Execute an OAuth-authenticated request with a single retry on 401. * On 401, attempts token refresh via handleOAuth401Error (same pattern as * withRetry.ts for v1/messages). If refresh succeeds, retries the request * once with the new token. If refresh fails or the retry also returns 401, * the 401 response is returned for handleErrorStatus to throw BridgeFatalError. */ async function withOAuthRetry<T>( fn: (accessToken: string) => Promise<{ status: number; data: T }>, context: string, ): Promise<{ status: number; data: T }> { const accessToken = resolveAuth() const response = await fn(accessToken)
if (response.status !== 401) { return response }
if (!deps.onAuth401) { debug(`[bridge:api] ${context}: 401 received, no refresh handler`) return response }
// Attempt token refresh — matches the pattern in withRetry.ts debug(`[bridge:api] ${context}: 401 received, attempting token refresh`) const refreshed = await deps.onAuth401(accessToken) if (refreshed) { debug(`[bridge:api] ${context}: Token refreshed, retrying request`) const newToken = resolveAuth() const retryResponse = await fn(newToken) if (retryResponse.status !== 401) { return retryResponse } debug(`[bridge:api] ${context}: Retry after refresh also got 401`) } else { debug(`[bridge:api] ${context}: Token refresh failed`) }
// Refresh failed — return 401 for handleErrorStatus to throw return response }
return { async registerBridgeEnvironment( config: BridgeConfig, ): Promise<{ environment_id: string; environment_secret: string }> { debug( `[bridge:api] POST /v1/environments/bridge bridgeId=${config.bridgeId}`, )
const response = await withOAuthRetry( (token: string) => axios.post<{ environment_id: string environment_secret: string }>( `${deps.baseUrl}/v1/environments/bridge`, { machine_name: config.machineName, directory: config.dir, branch: config.branch, git_repo_url: config.gitRepoUrl, // Advertise session capacity so claude.ai/code can show // "2/4 sessions" badges and only block the picker when // actually at capacity. Backends that don't yet accept // this field will silently ignore it. max_sessions: config.maxSessions, // worker_type lets claude.ai filter environments by origin // (e.g. assistant picker only shows assistant-mode workers). // Desktop cowork app sends "cowork"; we send a distinct value. metadata: { worker_type: config.workerType }, // Idempotent re-registration: if we have a backend-issued // environment_id from a prior session (--session-id resume), // send it back so the backend reattaches instead of creating // a new env. The backend may still hand back a fresh ID if // the old one expired — callers must compare the response. ...(config.reuseEnvironmentId && { environment_id: config.reuseEnvironmentId, }), }, { headers: getHeaders(token), timeout: 15_000, validateStatus: status => status < 500, }, ), 'Registration', )
handleErrorStatus(response.status, response.data, 'Registration') debug( `[bridge:api] POST /v1/environments/bridge -> ${response.status} environment_id=${response.data.environment_id}`, ) debug( `[bridge:api] >>> ${debugBody({ machine_name: config.machineName, directory: config.dir, branch: config.branch, git_repo_url: config.gitRepoUrl, max_sessions: config.maxSessions, metadata: { worker_type: config.workerType } })}`, ) debug(`[bridge:api] <<< ${debugBody(response.data)}`) return response.data },
async pollForWork( environmentId: string, environmentSecret: string, signal?: AbortSignal, reclaimOlderThanMs?: number, ): Promise<WorkResponse | null> { validateBridgeId(environmentId, 'environmentId')
// Save and reset so errors break the "consecutive empty" streak. // Restored below when the response is truly empty. const prevEmptyPolls = consecutiveEmptyPolls consecutiveEmptyPolls = 0
const response = await axios.get<WorkResponse | null>( `${deps.baseUrl}/v1/environments/${environmentId}/work/poll`, { headers: getHeaders(environmentSecret), params: reclaimOlderThanMs !== undefined ? { reclaim_older_than_ms: reclaimOlderThanMs } : undefined, timeout: 10_000, signal, validateStatus: status => status < 500, }, )
handleErrorStatus(response.status, response.data, 'Poll')
// Empty body or null = no work available if (!response.data) { consecutiveEmptyPolls = prevEmptyPolls + 1 if ( consecutiveEmptyPolls === 1 || consecutiveEmptyPolls % EMPTY_POLL_LOG_INTERVAL === 0 ) { debug( `[bridge:api] GET .../work/poll -> ${response.status} (no work, ${consecutiveEmptyPolls} consecutive empty polls)`, ) } return null }
debug( `[bridge:api] GET .../work/poll -> ${response.status} workId=${response.data.id} type=${response.data.data?.type}${response.data.data?.id ? ` sessionId=${response.data.data.id}` : ''}`, ) debug(`[bridge:api] <<< ${debugBody(response.data)}`) return response.data },
async acknowledgeWork( environmentId: string, workId: string, sessionToken: string, ): Promise<void> { validateBridgeId(environmentId, 'environmentId') validateBridgeId(workId, 'workId')
debug(`[bridge:api] POST .../work/${workId}/ack`)
const response = await axios.post( `${deps.baseUrl}/v1/environments/${environmentId}/work/${workId}/ack`, {}, { headers: getHeaders(sessionToken), timeout: 10_000, validateStatus: s => s < 500, }, )
handleErrorStatus(response.status, response.data, 'Acknowledge') debug(`[bridge:api] POST .../work/${workId}/ack -> ${response.status}`) },
async stopWork( environmentId: string, workId: string, force: boolean, ): Promise<void> { validateBridgeId(environmentId, 'environmentId') validateBridgeId(workId, 'workId')
debug(`[bridge:api] POST .../work/${workId}/stop force=${force}`)
const response = await withOAuthRetry( (token: string) => axios.post( `${deps.baseUrl}/v1/environments/${environmentId}/work/${workId}/stop`, { force }, { headers: getHeaders(token), timeout: 10_000, validateStatus: s => s < 500, }, ), 'StopWork', )
handleErrorStatus(response.status, response.data, 'StopWork') debug(`[bridge:api] POST .../work/${workId}/stop -> ${response.status}`) },
async deregisterEnvironment(environmentId: string): Promise<void> { validateBridgeId(environmentId, 'environmentId')
debug(`[bridge:api] DELETE /v1/environments/bridge/${environmentId}`)
const response = await withOAuthRetry( (token: string) => axios.delete( `${deps.baseUrl}/v1/environments/bridge/${environmentId}`, { headers: getHeaders(token), timeout: 10_000, validateStatus: s => s < 500, }, ), 'Deregister', )
handleErrorStatus(response.status, response.data, 'Deregister') debug( `[bridge:api] DELETE /v1/environments/bridge/${environmentId} -> ${response.status}`, ) },
async archiveSession(sessionId: string): Promise<void> { validateBridgeId(sessionId, 'sessionId')
debug(`[bridge:api] POST /v1/sessions/${sessionId}/archive`)
const response = await withOAuthRetry( (token: string) => axios.post( `${deps.baseUrl}/v1/sessions/${sessionId}/archive`, {}, { headers: getHeaders(token), timeout: 10_000, validateStatus: s => s < 500, }, ), 'ArchiveSession', )
// 409 = already archived (idempotent, not an error) if (response.status === 409) { debug( `[bridge:api] POST /v1/sessions/${sessionId}/archive -> 409 (already archived)`, ) return }
handleErrorStatus(response.status, response.data, 'ArchiveSession') debug( `[bridge:api] POST /v1/sessions/${sessionId}/archive -> ${response.status}`, ) },
async reconnectSession( environmentId: string, sessionId: string, ): Promise<void> { validateBridgeId(environmentId, 'environmentId') validateBridgeId(sessionId, 'sessionId')
debug( `[bridge:api] POST /v1/environments/${environmentId}/bridge/reconnect session_id=${sessionId}`, )
const response = await withOAuthRetry( (token: string) => axios.post( `${deps.baseUrl}/v1/environments/${environmentId}/bridge/reconnect`, { session_id: sessionId }, { headers: getHeaders(token), timeout: 10_000, validateStatus: s => s < 500, }, ), 'ReconnectSession', )
handleErrorStatus(response.status, response.data, 'ReconnectSession') debug(`[bridge:api] POST .../bridge/reconnect -> ${response.status}`) },
async heartbeatWork( environmentId: string, workId: string, sessionToken: string, ): Promise<{ lease_extended: boolean; state: string }> { validateBridgeId(environmentId, 'environmentId') validateBridgeId(workId, 'workId')
debug(`[bridge:api] POST .../work/${workId}/heartbeat`)
const response = await axios.post<{ lease_extended: boolean state: string last_heartbeat: string ttl_seconds: number }>( `${deps.baseUrl}/v1/environments/${environmentId}/work/${workId}/heartbeat`, {}, { headers: getHeaders(sessionToken), timeout: 10_000, validateStatus: s => s < 500, }, )
handleErrorStatus(response.status, response.data, 'Heartbeat') debug( `[bridge:api] POST .../work/${workId}/heartbeat -> ${response.status} lease_extended=${response.data.lease_extended} state=${response.data.state}`, ) return response.data },
async sendPermissionResponseEvent( sessionId: string, event: PermissionResponseEvent, sessionToken: string, ): Promise<void> { validateBridgeId(sessionId, 'sessionId')
debug( `[bridge:api] POST /v1/sessions/${sessionId}/events type=${event.type}`, )
const response = await axios.post( `${deps.baseUrl}/v1/sessions/${sessionId}/events`, { events: [event] }, { headers: getHeaders(sessionToken), timeout: 10_000, validateStatus: s => s < 500, }, )
handleErrorStatus( response.status, response.data, 'SendPermissionResponseEvent', ) debug( `[bridge:api] POST /v1/sessions/${sessionId}/events -> ${response.status}`, ) debug(`[bridge:api] >>> ${debugBody({ events: [event] })}`) debug(`[bridge:api] <<< ${debugBody(response.data)}`) }, }}
function handleErrorStatus( status: number, data: unknown, context: string,): void { if (status === 200 || status === 204) { return } const detail = extractErrorDetail(data) const errorType = extractErrorTypeFromData(data) switch (status) { case 401: throw new BridgeFatalError( `${context}: Authentication failed (401)${detail ? `: ${detail}` : ''}. ${BRIDGE_LOGIN_INSTRUCTION}`, 401, errorType, ) case 403: throw new BridgeFatalError( isExpiredErrorType(errorType) ? 'Remote Control session has expired. Please restart with `claude remote-control` or /remote-control.' : `${context}: Access denied (403)${detail ? `: ${detail}` : ''}. Check your organization permissions.`, 403, errorType, ) case 404: throw new BridgeFatalError( detail ?? `${context}: Not found (404). Remote Control may not be available for this organization.`, 404, errorType, ) case 410: throw new BridgeFatalError( detail ?? 'Remote Control session has expired. Please restart with `claude remote-control` or /remote-control.', 410, errorType ?? 'environment_expired', ) case 429: throw new Error(`${context}: Rate limited (429). Polling too frequently.`) default: throw new Error( `${context}: Failed with status ${status}${detail ? `: ${detail}` : ''}`, ) }}
/** Check whether an error type string indicates a session/environment expiry. */export function isExpiredErrorType(errorType: string | undefined): boolean { if (!errorType) { return false } return errorType.includes('expired') || errorType.includes('lifetime')}
/** * Check whether a BridgeFatalError is a suppressible 403 permission error. * These are 403 errors for scopes like 'external_poll_sessions' or operations * like StopWork that fail because the user's role lacks 'environments:manage'. * They don't affect core functionality and shouldn't be shown to users. */export function isSuppressible403(err: BridgeFatalError): boolean { if (err.status !== 403) { return false } return ( err.message.includes('external_poll_sessions') || err.message.includes('environments:manage') )}
function extractErrorTypeFromData(data: unknown): string | undefined { if (data && typeof data === 'object') { if ( 'error' in data && data.error && typeof data.error === 'object' && 'type' in data.error && typeof data.error.type === 'string' ) { return data.error.type } } return undefined}