feat: queue auto-peer requests for admin approval when autoAcceptPeering is off

This commit is contained in:
Jannis Braun
2026-04-20 14:54:35 +02:00
parent 9877d78a3a
commit 5e48d67cb0
+90
View File
@@ -295,6 +295,11 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
body: JSON.stringify({ body: JSON.stringify({
sourceOrigin: localOrigin, sourceOrigin: localOrigin,
hmacSecret, hmacSecret,
instanceName: db
.select({ name: schema.instanceSettings.instanceName })
.from(schema.instanceSettings)
.where(eq(schema.instanceSettings.id, 1))
.get()?.name ?? undefined,
}), }),
signal: AbortSignal.timeout(10_000), signal: AbortSignal.timeout(10_000),
}); });
@@ -405,12 +410,65 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
.get(); .get();
if (!localPending) { if (!localPending) {
// Check if this origin is blocked (previously denied)
const blockedPeer = db
.select({ id: schema.federationPeers.id })
.from(schema.federationPeers)
.where(
and(
eq(schema.federationPeers.origin, sourceOrigin),
eq(schema.federationPeers.status, 'rejected'),
),
)
.get();
if (blockedPeer) {
return reply.code(403).send({ return reply.code(403).send({
error: 'This instance requires manual peering approval', error: 'This instance requires manual peering approval',
code: 'PEERING_REQUIRES_APPROVAL', code: 'PEERING_REQUIRES_APPROVAL',
statusCode: 403, statusCode: 403,
}); });
} }
// Queue for admin approval — upsert into peer_approval_requests
const { instanceName: reqInstanceName } = request.body as { instanceName?: string };
const now = Date.now();
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
const existingRequest = db
.select({ id: schema.peerApprovalRequests.id })
.from(schema.peerApprovalRequests)
.where(eq(schema.peerApprovalRequests.origin, sourceOrigin))
.get();
if (existingRequest) {
db.update(schema.peerApprovalRequests)
.set({
instanceName: reqInstanceName ?? null,
hmacSecret,
requestedAt: now,
expiresAt: now + THIRTY_DAYS_MS,
})
.where(eq(schema.peerApprovalRequests.id, existingRequest.id))
.run();
} else {
db.insert(schema.peerApprovalRequests)
.values({
id: generateSnowflake(),
origin: sourceOrigin,
instanceName: reqInstanceName ?? null,
hmacSecret,
requestedAt: now,
expiresAt: now + THIRTY_DAYS_MS,
})
.run();
}
return reply.code(202).send({
queued: true,
message: 'Request queued for admin approval',
});
}
} }
// Check if peer already exists // Check if peer already exists
@@ -453,6 +511,27 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
return reply.code(200).send({ accepted: true }); return reply.code(200).send({ accepted: true });
} }
if (existing.status === 'awaiting_approval') {
// Remote admin approved — this is a fresh handshake from them.
db.update(schema.federationPeers)
.set({
hmacSecret,
status: 'active',
lastSeenAt: Date.now(),
})
.where(eq(schema.federationPeers.id, existing.id))
.run();
// Broadcast activation
for (const uid of connectionManager.getAllOnlineUserIds()) {
connectionManager.sendToUser(uid, {
type: 'federation_peer_active' as const,
peerOrigin: sourceOrigin,
});
}
return reply.code(200).send({ accepted: true });
}
// Pending — update with new secret and activate // Pending — update with new secret and activate
db.update(schema.federationPeers) db.update(schema.federationPeers)
.set({ .set({
@@ -511,11 +590,22 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
const { ensurePeered } = await import('../utils/federationPeering.js'); const { ensurePeered } = await import('../utils/federationPeering.js');
const result = await ensurePeered(remoteOrigin); const result = await ensurePeered(remoteOrigin);
// NOTE: The internal EnsurePeeredResult status names differ from the client-facing
// peeringStatus values. The mapping:
// 'active' → 'active' (peer is live)
// 'rejected' → 'rejected' (permanently blocked)
// 'pending' → 'awaiting_approval' (queued on remote, waiting for admin)
// 'failed' → 'pending' (transient error, will retry automatically)
// The internal 'pending' means "we got a 202 from the remote — admin hasn't acted yet",
// while 'failed' means "network/timeout — the outbox worker will retry next tick".
// The client sees 'awaiting_approval' (actionable info) vs 'pending' (transient, will resolve).
switch (result.status) { switch (result.status) {
case 'active': case 'active':
return reply.code(200).send({ peeringStatus: 'active', peerId: result.peerId }); return reply.code(200).send({ peeringStatus: 'active', peerId: result.peerId });
case 'rejected': case 'rejected':
return reply.code(200).send({ peeringStatus: 'rejected', error: result.error }); return reply.code(200).send({ peeringStatus: 'rejected', error: result.error });
case 'pending':
return reply.code(200).send({ peeringStatus: 'awaiting_approval', error: result.error });
case 'failed': case 'failed':
return reply.code(200).send({ peeringStatus: 'pending', error: result.error }); return reply.code(200).send({ peeringStatus: 'pending', error: result.error });
default: default: