From 072858cbbb53800dd2e2a67aefd68cdf1f8736a3 Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Mon, 20 Apr 2026 17:54:00 +0200 Subject: [PATCH] fix: multiple federation peering bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. queueOutboxEvent no longer creates pending peer placeholders when autoAcceptPeering is disabled — prevents bypassing the admin's peering control 2. Approval endpoint checks for 202 before response.ok — when the remote also has autoAcceptPeering off, sets peer to awaiting_approval instead of incorrectly activating it 3. awaiting_approval status added to Federation panel UI — status label, colors, filter options so these peers are visible and manageable --- packages/server/src/routes/federation.ts | 18 ++++++++++++++++++ packages/server/src/utils/federationOutbox.ts | 11 +++++++++++ .../instanceSettingsPanels/FederationPanel.tsx | 11 +++++++---- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/packages/server/src/routes/federation.ts b/packages/server/src/routes/federation.ts index c343411e..841f3dc0 100644 --- a/packages/server/src/routes/federation.ts +++ b/packages/server/src/routes/federation.ts @@ -996,6 +996,24 @@ export async function federationRoutes(app: FastifyInstance): Promise { signal: AbortSignal.timeout(10_000), }); + if (response.status === 202) { + // Remote instance also has autoAcceptPeering off — they queued our request. + // Don't activate our peer. Set to awaiting_approval until their admin also approves. + db.update(schema.federationPeers) + .set({ status: 'awaiting_approval' }) + .where(eq(schema.federationPeers.id, peerId)) + .run(); + // Delete the approval request since we already acted on it + db.delete(schema.peerApprovalRequests) + .where(eq(schema.peerApprovalRequests.id, id)) + .run(); + return reply.code(200).send({ + success: true, + awaitingRemoteApproval: true, + message: 'Remote instance also requires admin approval. Your request has been queued on their side.', + }); + } + if (!response.ok) { let errorMessage = `Remote instance rejected handshake (HTTP ${response.status})`; try { diff --git a/packages/server/src/utils/federationOutbox.ts b/packages/server/src/utils/federationOutbox.ts index 847fb459..64d9d7ec 100644 --- a/packages/server/src/utils/federationOutbox.ts +++ b/packages/server/src/utils/federationOutbox.ts @@ -152,12 +152,23 @@ export function queueOutboxEvent( : peers; // For targeted origins with no existing peer record, create pending placeholders + // (only when autoAcceptPeering is enabled — otherwise the admin controls all peering) if (targetPeerOrigins) { + const autoAcceptSettings = db + .select({ autoAcceptPeering: schema.instanceSettings.autoAcceptPeering }) + .from(schema.instanceSettings) + .where(eq(schema.instanceSettings.id, 1)) + .get(); + const autoAcceptPeering = (autoAcceptSettings?.autoAcceptPeering ?? 1) === 1; + const matchedOrigins = new Set(matchedPeers.map(p => p.origin)); for (const origin of targetPeerOrigins) { if (matchedOrigins.has(origin)) continue; + // Don't auto-create placeholders when autoAcceptPeering is off + if (!autoAcceptPeering) continue; + // Check if there's a rejected/revoked peer we should skip const existingPeer = db .select({ status: schema.federationPeers.status }) diff --git a/packages/web/src/components/modals/instanceSettingsPanels/FederationPanel.tsx b/packages/web/src/components/modals/instanceSettingsPanels/FederationPanel.tsx index 6198c113..bdab5806 100644 --- a/packages/web/src/components/modals/instanceSettingsPanels/FederationPanel.tsx +++ b/packages/web/src/components/modals/instanceSettingsPanels/FederationPanel.tsx @@ -173,6 +173,7 @@ function peerStatusColor(status: string): string { case 'pending': return 'bg-accent-lavender/15 text-accent-lavender'; case 'unreachable': return 'bg-accent-amber/15 text-accent-amber'; case 'rejected': return 'bg-accent-rose/15 text-accent-rose'; + case 'awaiting_approval': return 'bg-accent-amber/15 text-accent-amber'; case 'revoked': return 'bg-white/5 text-txt-tertiary'; default: return 'bg-white/5 text-txt-tertiary'; } @@ -184,6 +185,7 @@ function peerStatusDotColor(status: string): string { case 'pending': return 'bg-accent-lavender'; case 'unreachable': return 'bg-accent-amber'; case 'rejected': return 'bg-accent-rose'; + case 'awaiting_approval': return 'bg-accent-amber'; default: return 'bg-txt-tertiary'; } } @@ -195,13 +197,14 @@ function peerStatusLabel(status: string): string { case 'unreachable': return 'Unreachable'; case 'rejected': return 'Rejected (auto-peering denied)'; case 'revoked': return 'Revoked'; + case 'awaiting_approval': return 'Awaiting Approval'; default: return status; } } type PeerView = 'active' | 'revoked'; type SortBy = 'name' | 'lastSeen' | 'dateAdded' | 'failures'; -type StatusFilter = 'active' | 'unreachable' | 'pending' | 'rejected'; +type StatusFilter = 'active' | 'unreachable' | 'pending' | 'rejected' | 'awaiting_approval'; // ─── Filter Dropdown ───────────────────────────────────────────────────────── @@ -263,7 +266,7 @@ function FilterDropdown({ {view === 'active' && ( <>
Status
- {(['active', 'unreachable', 'pending', 'rejected'] as StatusFilter[]).map((s) => ( + {(['active', 'unreachable', 'pending', 'rejected', 'awaiting_approval'] as StatusFilter[]).map((s) => ( ))}
@@ -734,7 +737,7 @@ export function FederationPanel({ onApprovalCountChange }: { onApprovalCountChan const [peersLoading, setPeersLoading] = useState(false); const [peersError, setPeersError] = useState(''); const [view, setView] = useState('active'); - const [statusFilter, setStatusFilter] = useState>(new Set(['active', 'unreachable', 'pending', 'rejected'])); + const [statusFilter, setStatusFilter] = useState>(new Set(['active', 'unreachable', 'pending', 'rejected', 'awaiting_approval'])); const [sortBy, setSortBy] = useState('name'); const [expandedPeerId, setExpandedPeerId] = useState(null);