164 lines
4.6 KiB
TypeScript
164 lines
4.6 KiB
TypeScript
/**
|
|
* 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<string, string> {
|
|
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<string | null> {
|
|
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,
|
|
): Promise<RemoteCredentials | null> {
|
|
const url = `${baseUrl}/v1/code/sessions/${sessionId}/bridge`
|
|
let response
|
|
try {
|
|
response = await axios.post(
|
|
url,
|
|
{},
|
|
{
|
|
headers: oauthHeaders(accessToken),
|
|
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,
|
|
}
|
|
}
|