feat: queue auto-peer requests for admin approval when autoAcceptPeering is off
This commit is contained in:
@@ -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,10 +410,63 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
.get();
|
.get();
|
||||||
|
|
||||||
if (!localPending) {
|
if (!localPending) {
|
||||||
return reply.code(403).send({
|
// Check if this origin is blocked (previously denied)
|
||||||
error: 'This instance requires manual peering approval',
|
const blockedPeer = db
|
||||||
code: 'PEERING_REQUIRES_APPROVAL',
|
.select({ id: schema.federationPeers.id })
|
||||||
statusCode: 403,
|
.from(schema.federationPeers)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(schema.federationPeers.origin, sourceOrigin),
|
||||||
|
eq(schema.federationPeers.status, 'rejected'),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (blockedPeer) {
|
||||||
|
return reply.code(403).send({
|
||||||
|
error: 'This instance requires manual peering approval',
|
||||||
|
code: 'PEERING_REQUIRES_APPROVAL',
|
||||||
|
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',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user