/** * Thin HTTP wrappers for the CCR v2 code-session API. * * Separate file from remoteBridgeCore.ts so the SDK /bridge subpath can * export createCodeSession + fetchRemoteCredentials without bundling the * heavy CLI tree (analytics, transport, etc.). Callers supply explicit * accessToken + baseUrl — no implicit auth or config reads. */ import axios from 'axios' import { logForDebugging } from '../utils/debug.js' import { errorMessage } from '../utils/errors.js' import { jsonStringify } from '../utils/slowOperations.js' import { extractErrorDetail } from './debugUtils.js' const ANTHROPIC_VERSION = '2023-06-01' function oauthHeaders(accessToken: string): Record { return { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', 'anthropic-version': ANTHROPIC_VERSION, } } export async function createCodeSession( baseUrl: string, accessToken: string, title: string, timeoutMs: number, tags?: string[], ): Promise { const url = `${baseUrl}/v1/code/sessions` let response try { response = await axios.post( url, // bridge: {} is the positive signal for the oneof runner — omitting it // (or sending environment_id: "") now 400s. BridgeRunner is an empty // message today; it's a placeholder for future bridge-specific options. { title, bridge: {}, ...(tags?.length ? { tags } : {}) }, { headers: oauthHeaders(accessToken), timeout: timeoutMs, validateStatus: s => s < 500, }, ) } catch (err: unknown) { logForDebugging( `[code-session] Session create request failed: ${errorMessage(err)}`, ) return null } if (response.status !== 200 && response.status !== 201) { const detail = extractErrorDetail(response.data) logForDebugging( `[code-session] Session create failed ${response.status}${detail ? `: ${detail}` : ''}`, ) return null } const data: unknown = response.data if ( !data || typeof data !== 'object' || !('session' in data) || !data.session || typeof data.session !== 'object' || !('id' in data.session) || typeof data.session.id !== 'string' || !data.session.id.startsWith('cse_') ) { logForDebugging( `[code-session] No session.id (cse_*) in response: ${jsonStringify(data).slice(0, 200)}`, ) return null } return data.session.id } /** * Credentials from POST /bridge. JWT is opaque — do not decode. * Each /bridge call bumps worker_epoch server-side (it IS the register). */ export type RemoteCredentials = { worker_jwt: string api_base_url: string expires_in: number worker_epoch: number } export async function fetchRemoteCredentials( sessionId: string, baseUrl: string, accessToken: string, timeoutMs: number, trustedDeviceToken?: string, ): Promise { const url = `${baseUrl}/v1/code/sessions/${sessionId}/bridge` const headers = oauthHeaders(accessToken) if (trustedDeviceToken) { headers['X-Trusted-Device-Token'] = trustedDeviceToken } let response try { response = await axios.post( url, {}, { headers, timeout: timeoutMs, validateStatus: s => s < 500, }, ) } catch (err: unknown) { logForDebugging( `[code-session] /bridge request failed: ${errorMessage(err)}`, ) return null } if (response.status !== 200) { const detail = extractErrorDetail(response.data) logForDebugging( `[code-session] /bridge failed ${response.status}${detail ? `: ${detail}` : ''}`, ) return null } const data: unknown = response.data if ( data === null || typeof data !== 'object' || !('worker_jwt' in data) || typeof data.worker_jwt !== 'string' || !('expires_in' in data) || typeof data.expires_in !== 'number' || !('api_base_url' in data) || typeof data.api_base_url !== 'string' || !('worker_epoch' in data) ) { logForDebugging( `[code-session] /bridge response malformed (need worker_jwt, expires_in, api_base_url, worker_epoch): ${jsonStringify(data).slice(0, 200)}`, ) return null } // protojson serializes int64 as a string to avoid JS precision loss; // Go may also return a number depending on encoder settings. const rawEpoch = data.worker_epoch const epoch = typeof rawEpoch === 'string' ? Number(rawEpoch) : rawEpoch if ( typeof epoch !== 'number' || !Number.isFinite(epoch) || !Number.isSafeInteger(epoch) ) { logForDebugging( `[code-session] /bridge worker_epoch invalid: ${jsonStringify(rawEpoch)}`, ) return null } return { worker_jwt: data.worker_jwt, api_base_url: data.api_base_url, expires_in: data.expires_in, worker_epoch: epoch, } }