Skip to content

Commit f2ce963

Browse files
committed
fix: replace resolvePeerAvatarUrl with localPeerAvatarUrl for avatar URL handling and add avatar proxy endpoint
1 parent c95c38f commit f2ce963

File tree

4 files changed

+49
-10
lines changed

4 files changed

+49
-10
lines changed

packages/node/src/api/routes/contacts.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Router } from '../router.js';
44
import { handleError } from '../route-utils.js';
55
import { ok, created, noContent } from '../response.js';
66
import type { RuntimeContext } from '../types.js';
7-
import { resolvePeerAvatarUrl } from '../../utils/avatar-url.js';
7+
import { localPeerAvatarUrl } from '../../utils/avatar-url.js';
88

99
export function contactRoutes(ctx: RuntimeContext): Router {
1010
const router = new Router();
@@ -13,9 +13,10 @@ export function contactRoutes(ctx: RuntimeContext): Router {
1313
try {
1414
const contacts = ctx.contactService.listContacts().map((contact) => {
1515
const peer = ctx.peerProfileRepository.get(contact.did);
16+
const rawAvatarUrl = peer?.avatarUrl ?? contact.avatarUrl;
1617
return {
1718
...contact,
18-
avatarUrl: resolvePeerAvatarUrl(contact.avatarUrl, peer?.nodeUrl),
19+
avatarUrl: localPeerAvatarUrl(contact.did, rawAvatarUrl),
1920
};
2021
});
2122
ok(res, contacts, { self: '/api/v1/contacts' });
@@ -31,11 +32,12 @@ export function contactRoutes(ctx: RuntimeContext): Router {
3132
throw new TelagentError(ErrorCodes.NOT_FOUND, `Contact not found: ${params.did}`);
3233
}
3334
const peer = ctx.peerProfileRepository.get(params.did);
35+
const rawAvatarUrl = peer?.avatarUrl ?? contact.avatarUrl;
3436
ok(
3537
res,
3638
{
3739
...contact,
38-
avatarUrl: resolvePeerAvatarUrl(contact.avatarUrl, peer?.nodeUrl),
40+
avatarUrl: localPeerAvatarUrl(params.did, rawAvatarUrl),
3941
},
4042
{ self: `/api/v1/contacts/${encodeURIComponent(params.did)}` },
4143
);

packages/node/src/api/routes/profile.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Router } from '../router.js';
77
import { handleError } from '../route-utils.js';
88
import { ok } from '../response.js';
99
import type { RuntimeContext } from '../types.js';
10-
import { resolvePeerAvatarUrl } from '../../utils/avatar-url.js';
10+
import { resolvePeerAvatarUrl, localPeerAvatarUrl } from '../../utils/avatar-url.js';
1111

1212
const 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) {

packages/node/src/services/message-service.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import type { ContactService } from './contact-service.js';
1414
import type { GroupService } from './group-service.js';
1515
import type { PeerProfileRepository } from '../storage/peer-profile-repository.js';
1616
import type { KeyLifecycleService, KeySuite } from './key-lifecycle-service.js';
17-
import { resolvePeerAvatarUrl } from '../utils/avatar-url.js';
17+
import { localPeerAvatarUrl } from '../utils/avatar-url.js';
1818
import { SequenceAllocator } from './sequence-allocator.js';
1919
import type {
2020
MailboxStore,
@@ -475,10 +475,8 @@ export class MessageService {
475475
const peer = this.peerProfileRepository?.get(item.peerDid as Parameters<PeerProfileRepository['get']>[0]);
476476
const contact = this.contactService?.getContact(item.peerDid);
477477
const displayName = peer?.nickname || contact?.displayName || item.displayName;
478-
const avatarUrl = resolvePeerAvatarUrl(
479-
peer?.avatarUrl ?? contact?.avatarUrl ?? item.avatarUrl ?? undefined,
480-
peer?.nodeUrl,
481-
) ?? null;
478+
const rawAvatarUrl = peer?.avatarUrl ?? contact?.avatarUrl ?? item.avatarUrl ?? undefined;
479+
const avatarUrl = (item.peerDid ? localPeerAvatarUrl(item.peerDid, rawAvatarUrl) : undefined) ?? null;
482480
if (displayName === item.displayName && avatarUrl === item.avatarUrl) return item;
483481
return { ...item, displayName, avatarUrl };
484482
}

packages/node/src/utils/avatar-url.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,13 @@ export function resolvePeerAvatarUrl(
1616

1717
return `${peerNodeUrl.replace(/\/$/, '')}${avatarUrl}`;
1818
}
19+
20+
/**
21+
* Returns the local proxy path the frontend should use to load a peer avatar.
22+
* The local node will proxy the request to the remote peer node, avoiding
23+
* cross-origin / firewall issues in the browser.
24+
*/
25+
export function localPeerAvatarUrl(did: string, avatarUrl: string | undefined): string | undefined {
26+
if (!avatarUrl) return undefined;
27+
return `/api/v1/profile/${encodeURIComponent(did)}/avatar`;
28+
}

0 commit comments

Comments
 (0)