@@ -133,21 +133,26 @@ export function profileRoutes(ctx: RuntimeContext): Router {
133133 } ) ;
134134
135135 // ── GET /avatar (public — no auth) ──────────────────────────────────────
136- router . get ( '/avatar' , async ( { res, url } ) => {
136+ // Returns raw binary by default. With ?format=base64, returns JSON { data, mimeType }
137+ // so the API proxy relay (text-only) can transport avatar data safely.
138+ router . get ( '/avatar' , async ( { res, query, url } ) => {
137139 try {
138140 const avatar = await ctx . selfProfileStore . loadAvatar ( ) ;
139141 if ( ! avatar ) {
140142 throw new TelagentError ( ErrorCodes . NOT_FOUND , 'No avatar uploaded' ) ;
141143 }
144+ if ( query . get ( 'format' ) === 'base64' ) {
145+ ok ( res , { data : avatar . data . toString ( 'base64' ) , mimeType : avatar . mimeType } , { self : '/api/v1/profile/avatar' } ) ;
146+ return ;
147+ }
142148 sendBinary ( res , 200 , avatar . data , avatar . mimeType ) ;
143149 } catch ( error ) {
144150 handleError ( res , error , url . pathname ) ;
145151 }
146152 } ) ;
147153
148154 // ── GET /:did/avatar (public — proxies peer avatar via local node) ────────
149- // Serves the cached avatar first (embedded in profile card via P2P).
150- // Falls back to fetching from the peer's HTTP endpoint if no local cache.
155+ // 1. Local cache → 2. Direct HTTP → 3. P2P relay → 404
151156 router . get ( '/:did/avatar' , async ( { res, params, url } ) => {
152157 try {
153158 const { did } = params ;
@@ -159,34 +164,54 @@ export function profileRoutes(ctx: RuntimeContext): Router {
159164 throw new TelagentError ( ErrorCodes . NOT_FOUND , `No avatar for did: ${ did } ` ) ;
160165 }
161166
162- // 1. Try local cache (avatar embedded in the P2P profile card) .
167+ // 1. Try local cache.
163168 const cached = ctx . peerProfileRepository . loadAvatar ( did ) ;
164169 if ( cached ) {
165170 sendBinary ( res , 200 , cached . data , cached . mimeType ) ;
166171 return ;
167172 }
168173
169- // 2. Fallback: fetch from the peer's HTTP endpoint .
174+ // 2. Try direct HTTP to the peer's node .
170175 const remoteUrl = resolvePeerAvatarUrl ( profile . avatarUrl , profile . nodeUrl ) ;
171- if ( ! remoteUrl || remoteUrl . startsWith ( '/' ) ) {
172- throw new TelagentError ( ErrorCodes . NOT_FOUND , `Cannot resolve avatar URL for did: ${ did } ` ) ;
173- }
174- let response : Response ;
175- try {
176- response = await fetch ( remoteUrl , { signal : AbortSignal . timeout ( 8000 ) } ) ;
177- } catch {
178- throw new TelagentError ( ErrorCodes . NOT_FOUND , `Remote node unreachable for avatar: ${ did } ` ) ;
179- }
180- if ( ! response . ok ) {
181- throw new TelagentError ( ErrorCodes . NOT_FOUND , `Remote avatar fetch failed: ${ response . status } ` ) ;
176+ if ( remoteUrl && ! remoteUrl . startsWith ( '/' ) ) {
177+ try {
178+ const response = await fetch ( remoteUrl , { signal : AbortSignal . timeout ( 5000 ) } ) ;
179+ if ( response . ok ) {
180+ const contentType = response . headers . get ( 'content-type' ) ?? 'image/jpeg' ;
181+ const data = Buffer . from ( await response . arrayBuffer ( ) ) ;
182+ try { ctx . peerProfileRepository . saveAvatar ( did , data , contentType ) ; } catch { /* best-effort */ }
183+ sendBinary ( res , 200 , data , contentType ) ;
184+ return ;
185+ }
186+ } catch {
187+ // Direct HTTP failed — fall through to P2P relay.
188+ }
182189 }
183- const contentType = response . headers . get ( 'content-type' ) ?? 'image/jpeg' ;
184- const data = Buffer . from ( await response . arrayBuffer ( ) ) ;
185190
186- // Cache for future requests.
187- try { ctx . peerProfileRepository . saveAvatar ( did , data , contentType ) ; } catch { /* best-effort */ }
191+ // 3. P2P relay: request avatar as base64 JSON through ClawNet.
192+ if ( ctx . apiProxyService ) {
193+ try {
194+ const proxyRes = await ctx . apiProxyService . proxyRequest (
195+ did , 'GET' , '/api/v1/profile/avatar?format=base64' ,
196+ { accept : 'application/json' } ,
197+ ) ;
198+ if ( proxyRes . status === 200 && proxyRes . body ) {
199+ const json = JSON . parse ( proxyRes . body ) as { data ?: { data ?: string ; mimeType ?: string } } ;
200+ const b64 = json . data ?. data ;
201+ const mimeType = json . data ?. mimeType ?? 'image/jpeg' ;
202+ if ( b64 ) {
203+ const data = Buffer . from ( b64 , 'base64' ) ;
204+ try { ctx . peerProfileRepository . saveAvatar ( did , data , mimeType ) ; } catch { /* best-effort */ }
205+ sendBinary ( res , 200 , data , mimeType ) ;
206+ return ;
207+ }
208+ }
209+ } catch {
210+ // P2P relay also failed — fall through to 404.
211+ }
212+ }
188213
189- sendBinary ( res , 200 , data , contentType ) ;
214+ throw new TelagentError ( ErrorCodes . NOT_FOUND , `Avatar unavailable for did: ${ did } ` ) ;
190215 } catch ( error ) {
191216 handleError ( res , error , url . pathname ) ;
192217 }
0 commit comments