From 618056659ed97ba74f09447887d5d819aaa5a4cf Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Sat, 25 Apr 2026 00:48:42 +0200 Subject: [PATCH] fix(federation): persist remote instance_name from /peer/initiate handshake response Mirrors the previous performHandshake fix for the admin-initiated path. /peer/initiate now parses the remote's instanceName from the /peer/accept response body and writes it alongside status='active'. --- .../src/routes/federation.peerAccept.test.ts | 59 ++++++++++++++++++- packages/server/src/routes/federation.ts | 16 ++++- 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/packages/server/src/routes/federation.peerAccept.test.ts b/packages/server/src/routes/federation.peerAccept.test.ts index 7cec45ce..b1fce8be 100644 --- a/packages/server/src/routes/federation.peerAccept.test.ts +++ b/packages/server/src/routes/federation.peerAccept.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import Fastify, { type FastifyInstance } from 'fastify'; import Database from 'better-sqlite3'; import { drizzle } from 'drizzle-orm/better-sqlite3'; @@ -298,3 +298,60 @@ describe('POST /api/federation/peer/accept — response body carries instanceNam expect(body.instanceName).toBeNull(); }); }); + +describe('POST /api/federation/peer/initiate — persists remote instanceName from handshake response', () => { + let app: FastifyInstance; + + beforeEach(async () => { + sqlite = new Database(':memory:'); + testDb = drizzle(sqlite, { schema }); + applyMigrations(sqlite); + seedInstanceSettings('Local Backspace'); + app = await buildApp(); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('writes remote.instanceName when remote /peer/accept succeeds', async () => { + vi.stubGlobal('fetch', vi.fn(async () => + new Response(JSON.stringify({ accepted: true, instanceName: 'Remote Backspace' }), { + status: 200, + headers: { 'content-type': 'application/json' }, + }), + )); + + const response = await app.inject({ + method: 'POST', + url: '/api/federation/peer/initiate', + payload: { remoteOrigin: 'https://remote.example' }, + }); + + expect(response.statusCode).toBe(200); + const row = testDb.select().from(schema.federationPeers) + .where(eq(schema.federationPeers.origin, 'https://remote.example')).get(); + expect(row?.status).toBe('active'); + expect(row?.instanceName).toBe('Remote Backspace'); + }); + + it('writes null instanceName when remote response omits the field', async () => { + vi.stubGlobal('fetch', vi.fn(async () => + new Response(JSON.stringify({ accepted: true }), { + status: 200, + headers: { 'content-type': 'application/json' }, + }), + )); + + const response = await app.inject({ + method: 'POST', + url: '/api/federation/peer/initiate', + payload: { remoteOrigin: 'https://remote.example' }, + }); + + expect(response.statusCode).toBe(200); + const row = testDb.select().from(schema.federationPeers) + .where(eq(schema.federationPeers.origin, 'https://remote.example')).get(); + expect(row?.instanceName).toBeNull(); + }); +}); diff --git a/packages/server/src/routes/federation.ts b/packages/server/src/routes/federation.ts index 118b0abf..b66e444d 100644 --- a/packages/server/src/routes/federation.ts +++ b/packages/server/src/routes/federation.ts @@ -351,9 +351,21 @@ export async function federationRoutes(app: FastifyInstance): Promise { return reply.code(502).send({ error: errorMessage, statusCode: 502 }); } - // Remote accepted — activate the peer + // Remote accepted — activate the peer. Parse the remote's instanceName + // from the response body so the federation panel renders a friendly + // label. Tolerate omission and non-JSON bodies. + let remoteInstanceName: string | null = null; + try { + const body = (await response.json()) as { instanceName?: string | null }; + if (typeof body?.instanceName === 'string' && body.instanceName.length > 0) { + remoteInstanceName = body.instanceName; + } + } catch { + // Non-JSON body — leave null. + } + db.update(schema.federationPeers) - .set({ status: 'active', lastSeenAt: Date.now() }) + .set({ status: 'active', lastSeenAt: Date.now(), instanceName: remoteInstanceName }) .where(eq(schema.federationPeers.id, peerId)) .run(); connectionManager.sendToAdmins({ type: 'federation_peers_changed' as const });