@@ -22,95 +22,15 @@ import { assertAgentCardShape, type AgentCard, type PushResult } from './types.j
2222import { loadAgentId , signMessage , signChallenge } from './identity.js' ;
2323import { version } from './update-check.js' ;
2424import { validateBranchName , validateHttpUrl } from './validation.js' ;
25+ import { fetchWithTimeout , readResponseJson , readResponseText } from './http.js' ;
2526
2627// Client-declared signals (server stores but treats as untrusted)
2728const platformSignal = `${ platform ( ) } -${ arch ( ) } ` ;
2829const hostnameHash = createHash ( 'sha256' ) . update ( hostname ( ) ) . digest ( 'hex' ) ;
2930const FETCH_TIMEOUT_MS = 10_000 ;
30- const MAX_RESPONSE_BYTES = 256 * 1024 ;
3131const MAX_ERROR_BYTES = 16 * 1024 ;
3232const MAX_CHALLENGE_BYTES = 4096 ;
3333
34- async function fetchWithTimeout (
35- url : string ,
36- init : RequestInit = { } ,
37- label = 'Remote request' ,
38- ) : Promise < Response > {
39- const controller = new AbortController ( ) ;
40- let timedOut = false ;
41- const timeout = setTimeout ( ( ) => {
42- timedOut = true ;
43- controller . abort ( ) ;
44- } , FETCH_TIMEOUT_MS ) ;
45-
46- try {
47- return await fetch ( url , { ...init , signal : controller . signal } ) ;
48- } catch ( err ) {
49- if ( timedOut ) {
50- throw new Error ( `${ label } timed out after ${ FETCH_TIMEOUT_MS } ms` ) ;
51- }
52- throw err ;
53- } finally {
54- clearTimeout ( timeout ) ;
55- }
56- }
57-
58- async function readResponseText (
59- res : Response ,
60- label : string ,
61- maxBytes = MAX_RESPONSE_BYTES ,
62- ) : Promise < string > {
63- const length = res . headers . get ( 'content-length' ) ;
64- if ( length && Number . parseInt ( length , 10 ) > maxBytes ) {
65- throw new Error ( `${ label } exceeds ${ maxBytes } bytes` ) ;
66- }
67-
68- if ( ! res . body ) {
69- const text = await res . text ( ) ;
70- if ( new TextEncoder ( ) . encode ( text ) . byteLength > maxBytes ) {
71- throw new Error ( `${ label } exceeds ${ maxBytes } bytes` ) ;
72- }
73- return text ;
74- }
75-
76- const reader = res . body . getReader ( ) ;
77- const chunks : Uint8Array [ ] = [ ] ;
78- let total = 0 ;
79-
80- while ( true ) {
81- const { done, value } = await reader . read ( ) ;
82- if ( done ) break ;
83- if ( ! value ) continue ;
84- total += value . byteLength ;
85- if ( total > maxBytes ) {
86- await reader . cancel ( ) ;
87- throw new Error ( `${ label } exceeds ${ maxBytes } bytes` ) ;
88- }
89- chunks . push ( value ) ;
90- }
91-
92- const bytes = new Uint8Array ( total ) ;
93- let offset = 0 ;
94- for ( const chunk of chunks ) {
95- bytes . set ( chunk , offset ) ;
96- offset += chunk . byteLength ;
97- }
98- return new TextDecoder ( ) . decode ( bytes ) ;
99- }
100-
101- async function readResponseJson < T > (
102- res : Response ,
103- label : string ,
104- maxBytes = MAX_RESPONSE_BYTES ,
105- ) : Promise < T > {
106- const text = await readResponseText ( res , label , maxBytes ) ;
107- try {
108- return JSON . parse ( text ) as T ;
109- } catch {
110- throw new Error ( `${ label } is not valid JSON` ) ;
111- }
112- }
113-
11434function parseRemoteBranchList ( data : unknown ) : string [ ] {
11535 if ( ! data || typeof data !== 'object' || ! Array . isArray ( ( data as { branches ?: unknown } ) . branches ) ) {
11636 throw new Error ( 'Remote branch list has invalid shape' ) ;
@@ -223,7 +143,7 @@ export async function pushBranch(
223143 ...authHeaders ,
224144 } ,
225145 body,
226- } , `Push branch "${ branch } "` ) ;
146+ } , { label : `Push branch "${ branch } "` , timeoutMs : FETCH_TIMEOUT_MS } ) ;
227147
228148 if ( ! res . ok ) {
229149 const text = await readResponseText ( res , 'Push error response' , MAX_ERROR_BYTES ) ;
@@ -281,7 +201,7 @@ export async function listRemoteBranches(
281201
282202 const res = await fetchWithTimeout ( `${ apiBase } ${ path } ` , {
283203 headers : authHeaders ,
284- } , 'List remote branches' ) ;
204+ } , { label : 'List remote branches' , timeoutMs : FETCH_TIMEOUT_MS } ) ;
285205
286206 if ( ! res . ok ) {
287207 throw new Error ( `Failed to list remote branches: HTTP ${ res . status } ` ) ;
@@ -307,7 +227,7 @@ export async function deleteRemoteBranch(
307227 const res = await fetchWithTimeout ( `${ apiBase } ${ path } ` , {
308228 method : 'DELETE' ,
309229 headers : authHeaders ,
310- } , `Delete remote branch "${ branch } "` ) ;
230+ } , { label : `Delete remote branch "${ branch } "` , timeoutMs : FETCH_TIMEOUT_MS } ) ;
311231
312232 if ( ! res . ok ) {
313233 const text = await readResponseText ( res , 'Delete error response' , MAX_ERROR_BYTES ) ;
@@ -348,7 +268,7 @@ export async function fetchBranchCard(
348268 url += `?branch=${ encodeURIComponent ( branch ) } ` ;
349269 }
350270
351- const res = await fetchWithTimeout ( url , undefined , `Fetch branch "${ branch } "` ) ;
271+ const res = await fetchWithTimeout ( url , undefined , { label : `Fetch branch "${ branch } "` , timeoutMs : FETCH_TIMEOUT_MS } ) ;
352272
353273 // Main branch or already authorized
354274 if ( res . ok ) {
@@ -376,7 +296,7 @@ export async function fetchBranchCard(
376296 'X-Nit-Challenge' : challengeData . challenge ,
377297 'X-Nit-Signature' : signature ,
378298 } ,
379- } , `Fetch branch "${ branch } " after challenge` ) ;
299+ } , { label : `Fetch branch "${ branch } " after challenge` , timeoutMs : FETCH_TIMEOUT_MS } ) ;
380300
381301 if ( ! authRes . ok ) {
382302 const body = await readResponseText ( authRes , 'Challenge error response' , MAX_ERROR_BYTES ) ;
0 commit comments