source dump of claude code
at main 168 lines 4.8 kB view raw
1/** 2 * Thin HTTP wrappers for the CCR v2 code-session API. 3 * 4 * Separate file from remoteBridgeCore.ts so the SDK /bridge subpath can 5 * export createCodeSession + fetchRemoteCredentials without bundling the 6 * heavy CLI tree (analytics, transport, etc.). Callers supply explicit 7 * accessToken + baseUrl — no implicit auth or config reads. 8 */ 9 10import axios from 'axios' 11import { logForDebugging } from '../utils/debug.js' 12import { errorMessage } from '../utils/errors.js' 13import { jsonStringify } from '../utils/slowOperations.js' 14import { extractErrorDetail } from './debugUtils.js' 15 16const ANTHROPIC_VERSION = '2023-06-01' 17 18function oauthHeaders(accessToken: string): Record<string, string> { 19 return { 20 Authorization: `Bearer ${accessToken}`, 21 'Content-Type': 'application/json', 22 'anthropic-version': ANTHROPIC_VERSION, 23 } 24} 25 26export async function createCodeSession( 27 baseUrl: string, 28 accessToken: string, 29 title: string, 30 timeoutMs: number, 31 tags?: string[], 32): Promise<string | null> { 33 const url = `${baseUrl}/v1/code/sessions` 34 let response 35 try { 36 response = await axios.post( 37 url, 38 // bridge: {} is the positive signal for the oneof runner — omitting it 39 // (or sending environment_id: "") now 400s. BridgeRunner is an empty 40 // message today; it's a placeholder for future bridge-specific options. 41 { title, bridge: {}, ...(tags?.length ? { tags } : {}) }, 42 { 43 headers: oauthHeaders(accessToken), 44 timeout: timeoutMs, 45 validateStatus: s => s < 500, 46 }, 47 ) 48 } catch (err: unknown) { 49 logForDebugging( 50 `[code-session] Session create request failed: ${errorMessage(err)}`, 51 ) 52 return null 53 } 54 55 if (response.status !== 200 && response.status !== 201) { 56 const detail = extractErrorDetail(response.data) 57 logForDebugging( 58 `[code-session] Session create failed ${response.status}${detail ? `: ${detail}` : ''}`, 59 ) 60 return null 61 } 62 63 const data: unknown = response.data 64 if ( 65 !data || 66 typeof data !== 'object' || 67 !('session' in data) || 68 !data.session || 69 typeof data.session !== 'object' || 70 !('id' in data.session) || 71 typeof data.session.id !== 'string' || 72 !data.session.id.startsWith('cse_') 73 ) { 74 logForDebugging( 75 `[code-session] No session.id (cse_*) in response: ${jsonStringify(data).slice(0, 200)}`, 76 ) 77 return null 78 } 79 return data.session.id 80} 81 82/** 83 * Credentials from POST /bridge. JWT is opaque — do not decode. 84 * Each /bridge call bumps worker_epoch server-side (it IS the register). 85 */ 86export type RemoteCredentials = { 87 worker_jwt: string 88 api_base_url: string 89 expires_in: number 90 worker_epoch: number 91} 92 93export async function fetchRemoteCredentials( 94 sessionId: string, 95 baseUrl: string, 96 accessToken: string, 97 timeoutMs: number, 98 trustedDeviceToken?: string, 99): Promise<RemoteCredentials | null> { 100 const url = `${baseUrl}/v1/code/sessions/${sessionId}/bridge` 101 const headers = oauthHeaders(accessToken) 102 if (trustedDeviceToken) { 103 headers['X-Trusted-Device-Token'] = trustedDeviceToken 104 } 105 let response 106 try { 107 response = await axios.post( 108 url, 109 {}, 110 { 111 headers, 112 timeout: timeoutMs, 113 validateStatus: s => s < 500, 114 }, 115 ) 116 } catch (err: unknown) { 117 logForDebugging( 118 `[code-session] /bridge request failed: ${errorMessage(err)}`, 119 ) 120 return null 121 } 122 123 if (response.status !== 200) { 124 const detail = extractErrorDetail(response.data) 125 logForDebugging( 126 `[code-session] /bridge failed ${response.status}${detail ? `: ${detail}` : ''}`, 127 ) 128 return null 129 } 130 131 const data: unknown = response.data 132 if ( 133 data === null || 134 typeof data !== 'object' || 135 !('worker_jwt' in data) || 136 typeof data.worker_jwt !== 'string' || 137 !('expires_in' in data) || 138 typeof data.expires_in !== 'number' || 139 !('api_base_url' in data) || 140 typeof data.api_base_url !== 'string' || 141 !('worker_epoch' in data) 142 ) { 143 logForDebugging( 144 `[code-session] /bridge response malformed (need worker_jwt, expires_in, api_base_url, worker_epoch): ${jsonStringify(data).slice(0, 200)}`, 145 ) 146 return null 147 } 148 // protojson serializes int64 as a string to avoid JS precision loss; 149 // Go may also return a number depending on encoder settings. 150 const rawEpoch = data.worker_epoch 151 const epoch = typeof rawEpoch === 'string' ? Number(rawEpoch) : rawEpoch 152 if ( 153 typeof epoch !== 'number' || 154 !Number.isFinite(epoch) || 155 !Number.isSafeInteger(epoch) 156 ) { 157 logForDebugging( 158 `[code-session] /bridge worker_epoch invalid: ${jsonStringify(rawEpoch)}`, 159 ) 160 return null 161 } 162 return { 163 worker_jwt: data.worker_jwt, 164 api_base_url: data.api_base_url, 165 expires_in: data.expires_in, 166 worker_epoch: epoch, 167 } 168}