@@ -7,7 +7,7 @@ import { Router } from '../router.js';
77import { handleError } from '../route-utils.js' ;
88import { ok } from '../response.js' ;
99import type { RuntimeContext } from '../types.js' ;
10- import { resolvePeerAvatarUrl } from '../../utils/avatar-url.js' ;
10+ import { resolvePeerAvatarUrl , localPeerAvatarUrl } from '../../utils/avatar-url.js' ;
1111
1212const ALLOWED_MIME_TYPES = new Set ( [
1313 'image/jpeg' ,
@@ -144,6 +144,35 @@ export function profileRoutes(ctx: RuntimeContext): Router {
144144 }
145145 } ) ;
146146
147+ // ── GET /:did/avatar (public — proxies peer avatar via local node) ────────
148+ // The frontend should never fetch images directly from remote nodes (cross-origin
149+ // / firewall issues). This endpoint proxies the avatar from the peer's node URL.
150+ router . get ( '/:did/avatar' , async ( { res, params, url } ) => {
151+ try {
152+ const { did } = params ;
153+ if ( ! did ) {
154+ throw new TelagentError ( ErrorCodes . VALIDATION , 'did is required' ) ;
155+ }
156+ const profile = ctx . peerProfileRepository . get ( did ) ;
157+ if ( ! profile ?. avatarUrl ) {
158+ throw new TelagentError ( ErrorCodes . NOT_FOUND , `No avatar for did: ${ did } ` ) ;
159+ }
160+ const remoteUrl = resolvePeerAvatarUrl ( profile . avatarUrl , profile . nodeUrl ) ;
161+ if ( ! remoteUrl || remoteUrl . startsWith ( '/' ) ) {
162+ throw new TelagentError ( ErrorCodes . NOT_FOUND , `Cannot resolve avatar URL for did: ${ did } ` ) ;
163+ }
164+ const response = await fetch ( remoteUrl , { signal : AbortSignal . timeout ( 8000 ) } ) ;
165+ if ( ! response . ok ) {
166+ throw new TelagentError ( ErrorCodes . NOT_FOUND , `Remote avatar fetch failed: ${ response . status } ` ) ;
167+ }
168+ const contentType = response . headers . get ( 'content-type' ) ?? 'image/jpeg' ;
169+ const data = Buffer . from ( await response . arrayBuffer ( ) ) ;
170+ sendBinary ( res , 200 , data , contentType ) ;
171+ } catch ( error ) {
172+ handleError ( res , error , url . pathname ) ;
173+ }
174+ } ) ;
175+
147176 // ── GET /:did (public — no auth, returns cached peer profile) ────────────
148177 // On cache miss, fires a profile-card request via P2P so the next query will
149178 // hit the cache once the peer replies.
@@ -179,7 +208,7 @@ export function profileRoutes(ctx: RuntimeContext): Router {
179208 }
180209 const normalizedProfile = {
181210 ...profile ,
182- avatarUrl : resolvePeerAvatarUrl ( profile . avatarUrl , profile . nodeUrl ) ,
211+ avatarUrl : localPeerAvatarUrl ( did , profile . avatarUrl ) ,
183212 } ;
184213 ok ( res , normalizedProfile , { self : `/api/v1/profile/${ encodeURIComponent ( did ) } ` } ) ;
185214 } catch ( error ) {
0 commit comments