diff --git a/packages/server/src/routes/federation.peerAccept.test.ts b/packages/server/src/routes/federation.peerAccept.test.ts index e3c1929e..7cec45ce 100644 --- a/packages/server/src/routes/federation.peerAccept.test.ts +++ b/packages/server/src/routes/federation.peerAccept.test.ts @@ -227,3 +227,74 @@ describe('POST /api/federation/peer/accept — instance_name persistence', () => expect(row?.instanceName).toBeNull(); }); }); + +describe('POST /api/federation/peer/accept — response body carries instanceName', () => { + let app: FastifyInstance; + + beforeEach(async () => { + sqlite = new Database(':memory:'); + testDb = drizzle(sqlite, { schema }); + applyMigrations(sqlite); + seedInstanceSettings('Local Backspace'); + app = await buildApp(); + }); + + it('returns our own instanceName in the response body on new-peer accept', async () => { + const response = await app.inject({ + method: 'POST', + url: '/api/federation/peer/accept', + payload: { + sourceOrigin: 'https://remote.example', + hmacSecret: 'remote-secret', + instanceName: 'Remote Backspace', + }, + }); + + expect(response.statusCode).toBe(200); + const body = response.json() as { accepted: boolean; instanceName?: string | null }; + expect(body.accepted).toBe(true); + expect(body.instanceName).toBe('Local Backspace'); + }); + + it('returns our instanceName on the active idempotent path too', async () => { + testDb.insert(schema.federationPeers).values({ + id: 'peer-active', + origin: 'https://remote.example', + hmacSecret: 'existing-secret', + status: 'active', + instanceName: 'Original Name', + createdAt: Date.now(), + }).run(); + + const response = await app.inject({ + method: 'POST', + url: '/api/federation/peer/accept', + payload: { + sourceOrigin: 'https://remote.example', + hmacSecret: 'remote-secret', + instanceName: 'Remote Backspace', + }, + }); + + expect(response.statusCode).toBe(200); + const body = response.json() as { accepted: boolean; instanceName?: string | null }; + expect(body.instanceName).toBe('Local Backspace'); + }); + + it('returns instanceName: null when instanceSettings table is empty', async () => { + testDb.delete(schema.instanceSettings).run(); + + const response = await app.inject({ + method: 'POST', + url: '/api/federation/peer/accept', + payload: { + sourceOrigin: 'https://remote.example', + hmacSecret: 'remote-secret', + }, + }); + + expect(response.statusCode).toBe(200); + const body = response.json() as { accepted: boolean; instanceName?: string | null }; + expect(body.instanceName).toBeNull(); + }); +}); diff --git a/packages/server/src/routes/federation.ts b/packages/server/src/routes/federation.ts index f01d7291..118b0abf 100644 --- a/packages/server/src/routes/federation.ts +++ b/packages/server/src/routes/federation.ts @@ -421,16 +421,22 @@ export async function federationRoutes(app: FastifyInstance): Promise { const db = getDb(); + const settings = db + .select({ + instanceName: schema.instanceSettings.instanceName, + autoAcceptPeering: schema.instanceSettings.autoAcceptPeering, + }) + .from(schema.instanceSettings) + .where(eq(schema.instanceSettings.id, 1)) + .get(); + + const ourInstanceName = settings?.instanceName ?? null; + const autoAccept = settings?.autoAcceptPeering ?? 1; + // ── autoAcceptPeering gate ────────────────────────────────────────── // When auto-accept is disabled, only allow incoming accept requests // that correspond to a local pending peer (i.e., a local admin // initiated the handshake). Unsolicited requests are rejected. - const settings = db - .select({ autoAcceptPeering: schema.instanceSettings.autoAcceptPeering }) - .from(schema.instanceSettings) - .where(eq(schema.instanceSettings.id, 1)) - .get(); - const autoAccept = settings?.autoAcceptPeering ?? 1; if (autoAccept === 0) { // Check if the local admin already initiated or approved peering with this origin. @@ -536,7 +542,7 @@ export async function federationRoutes(app: FastifyInstance): Promise { // Legitimate recovery path: local admin clicks "Reset peering" → // row is deleted → remote's /peer/accept then lands on a // non-existent row and the normal handshake path runs. - return reply.code(200).send({ accepted: true }); + return reply.code(200).send({ accepted: true, instanceName: ourInstanceName }); } if (existing.status === 'revoked') { return reply.code(403).send({ @@ -570,7 +576,7 @@ export async function federationRoutes(app: FastifyInstance): Promise { console.error('[federation] onPeerActivated from /peer/accept (rejected override) failed:', err) ); - return reply.code(200).send({ accepted: true }); + return reply.code(200).send({ accepted: true, instanceName: ourInstanceName }); } if (existing.status === 'awaiting_approval') { // Remote admin approved — this is a fresh handshake from them. @@ -597,7 +603,7 @@ export async function federationRoutes(app: FastifyInstance): Promise { console.error('[federation] onPeerActivated from /peer/accept (awaiting_approval) failed:', err) ); - return reply.code(200).send({ accepted: true }); + return reply.code(200).send({ accepted: true, instanceName: ourInstanceName }); } // Pending — update with new secret and activate db.update(schema.federationPeers) @@ -615,7 +621,7 @@ export async function federationRoutes(app: FastifyInstance): Promise { console.error('[federation] onPeerActivated from /peer/accept (pending) failed:', err) ); - return reply.code(200).send({ accepted: true }); + return reply.code(200).send({ accepted: true, instanceName: ourInstanceName }); } // New peer — create and activate @@ -635,7 +641,7 @@ export async function federationRoutes(app: FastifyInstance): Promise { console.error('[federation] onPeerActivated from /peer/accept (new) failed:', err) ); - return reply.code(200).send({ accepted: true }); + return reply.code(200).send({ accepted: true, instanceName: ourInstanceName }); }, );