source dump of claude code
at main 127 lines 4.7 kB view raw
1import axios from 'axios' 2import { jsonParse, jsonStringify } from '../utils/slowOperations.js' 3import type { WorkSecret } from './types.js' 4 5/** Decode a base64url-encoded work secret and validate its version. */ 6export function decodeWorkSecret(secret: string): WorkSecret { 7 const json = Buffer.from(secret, 'base64url').toString('utf-8') 8 const parsed: unknown = jsonParse(json) 9 if ( 10 !parsed || 11 typeof parsed !== 'object' || 12 !('version' in parsed) || 13 parsed.version !== 1 14 ) { 15 throw new Error( 16 `Unsupported work secret version: ${parsed && typeof parsed === 'object' && 'version' in parsed ? parsed.version : 'unknown'}`, 17 ) 18 } 19 const obj = parsed as Record<string, unknown> 20 if ( 21 typeof obj.session_ingress_token !== 'string' || 22 obj.session_ingress_token.length === 0 23 ) { 24 throw new Error( 25 'Invalid work secret: missing or empty session_ingress_token', 26 ) 27 } 28 if (typeof obj.api_base_url !== 'string') { 29 throw new Error('Invalid work secret: missing api_base_url') 30 } 31 return parsed as WorkSecret 32} 33 34/** 35 * Build a WebSocket SDK URL from the API base URL and session ID. 36 * Strips the HTTP(S) protocol and constructs a ws(s):// ingress URL. 37 * 38 * Uses /v2/ for localhost (direct to session-ingress, no Envoy rewrite) 39 * and /v1/ for production (Envoy rewrites /v1/ → /v2/). 40 */ 41export function buildSdkUrl(apiBaseUrl: string, sessionId: string): string { 42 const isLocalhost = 43 apiBaseUrl.includes('localhost') || apiBaseUrl.includes('127.0.0.1') 44 const protocol = isLocalhost ? 'ws' : 'wss' 45 const version = isLocalhost ? 'v2' : 'v1' 46 const host = apiBaseUrl.replace(/^https?:\/\//, '').replace(/\/+$/, '') 47 return `${protocol}://${host}/${version}/session_ingress/ws/${sessionId}` 48} 49 50/** 51 * Compare two session IDs regardless of their tagged-ID prefix. 52 * 53 * Tagged IDs have the form {tag}_{body} or {tag}_staging_{body}, where the 54 * body encodes a UUID. CCR v2's compat layer returns `session_*` to v1 API 55 * clients (compat/convert.go:41) but the infrastructure layer (sandbox-gateway 56 * work queue, work poll response) uses `cse_*` (compat/CLAUDE.md:13). Both 57 * have the same underlying UUID. 58 * 59 * Without this, replBridge rejects its own session as "foreign" at the 60 * work-received check when the ccr_v2_compat_enabled gate is on. 61 */ 62export function sameSessionId(a: string, b: string): boolean { 63 if (a === b) return true 64 // The body is everything after the last underscore — this handles both 65 // `{tag}_{body}` and `{tag}_staging_{body}`. 66 const aBody = a.slice(a.lastIndexOf('_') + 1) 67 const bBody = b.slice(b.lastIndexOf('_') + 1) 68 // Guard against IDs with no underscore (bare UUIDs): lastIndexOf returns -1, 69 // slice(0) returns the whole string, and we already checked a === b above. 70 // Require a minimum length to avoid accidental matches on short suffixes 71 // (e.g. single-char tag remnants from malformed IDs). 72 return aBody.length >= 4 && aBody === bBody 73} 74 75/** 76 * Build a CCR v2 session URL from the API base URL and session ID. 77 * Unlike buildSdkUrl, this returns an HTTP(S) URL (not ws://) and points at 78 * /v1/code/sessions/{id} — the child CC will derive the SSE stream path 79 * and worker endpoints from this base. 80 */ 81export function buildCCRv2SdkUrl( 82 apiBaseUrl: string, 83 sessionId: string, 84): string { 85 const base = apiBaseUrl.replace(/\/+$/, '') 86 return `${base}/v1/code/sessions/${sessionId}` 87} 88 89/** 90 * Register this bridge as the worker for a CCR v2 session. 91 * Returns the worker_epoch, which must be passed to the child CC process 92 * so its CCRClient can include it in every heartbeat/state/event request. 93 * 94 * Mirrors what environment-manager does in the container path 95 * (api-go/environment-manager/cmd/cmd_task_run.go RegisterWorker). 96 */ 97export async function registerWorker( 98 sessionUrl: string, 99 accessToken: string, 100): Promise<number> { 101 const response = await axios.post( 102 `${sessionUrl}/worker/register`, 103 {}, 104 { 105 headers: { 106 Authorization: `Bearer ${accessToken}`, 107 'Content-Type': 'application/json', 108 'anthropic-version': '2023-06-01', 109 }, 110 timeout: 10_000, 111 }, 112 ) 113 // protojson serializes int64 as a string to avoid JS number precision loss; 114 // the Go side may also return a number depending on encoder settings. 115 const raw = response.data?.worker_epoch 116 const epoch = typeof raw === 'string' ? Number(raw) : raw 117 if ( 118 typeof epoch !== 'number' || 119 !Number.isFinite(epoch) || 120 !Number.isSafeInteger(epoch) 121 ) { 122 throw new Error( 123 `registerWorker: invalid worker_epoch in response: ${jsonStringify(response.data)}`, 124 ) 125 } 126 return epoch 127}