| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168 |
- /**
- * 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,
- trustedDeviceToken?: string,
- ): Promise<RemoteCredentials | null> {
- 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,
- }
- }
|