/** * Federation Controller * * API endpoints for instance identity and federation management. */ import { Controller, Get, Post, Patch, UseGuards, Logger, Req, Body, Param, Query, } from "@nestjs/common"; import { FederationService } from "./federation.service"; import { FederationAuditService } from "./audit.service"; import { ConnectionService } from "./connection.service"; import { FederationAgentService } from "./federation-agent.service"; import { AuthGuard } from "../auth/guards/auth.guard"; import { AdminGuard } from "../auth/guards/admin.guard"; import type { PublicInstanceIdentity } from "./types/instance.types"; import type { ConnectionDetails } from "./types/connection.types"; import type { CommandMessageDetails } from "./types/message.types"; import type { AuthenticatedRequest } from "../common/types/user.types"; import { InitiateConnectionDto, AcceptConnectionDto, RejectConnectionDto, DisconnectConnectionDto, IncomingConnectionRequestDto, } from "./dto/connection.dto"; import { UpdateInstanceDto } from "./dto/instance.dto"; import type { SpawnAgentCommandPayload } from "./types/federation-agent.types"; import { FederationConnectionStatus } from "@prisma/client"; @Controller("api/v1/federation") export class FederationController { private readonly logger = new Logger(FederationController.name); constructor( private readonly federationService: FederationService, private readonly auditService: FederationAuditService, private readonly connectionService: ConnectionService, private readonly federationAgentService: FederationAgentService ) {} /** * Get this instance's public identity * No authentication required - this is public information for federation */ @Get("instance") async getInstance(): Promise { this.logger.debug("GET /api/v1/federation/instance"); return this.federationService.getPublicIdentity(); } /** * Regenerate instance keypair * Requires system administrator privileges * Returns public identity only (private key never exposed in API) */ @Post("instance/regenerate-keys") @UseGuards(AuthGuard, AdminGuard) async regenerateKeys(@Req() req: AuthenticatedRequest): Promise { if (!req.user) { throw new Error("User not authenticated"); } this.logger.warn(`Admin user ${req.user.id} regenerating instance keypair`); const result = await this.federationService.regenerateKeypair(); // Audit log for security compliance this.auditService.logKeypairRegeneration(req.user.id, result.instanceId); return result; } /** * Update instance configuration * Requires system administrator privileges * Allows updating name, capabilities, and metadata * Returns public identity only (private key never exposed in API) */ @Patch("instance") @UseGuards(AuthGuard, AdminGuard) async updateInstanceConfiguration( @Req() req: AuthenticatedRequest, @Body() dto: UpdateInstanceDto ): Promise { if (!req.user) { throw new Error("User not authenticated"); } this.logger.log(`Admin user ${req.user.id} updating instance configuration`); const result = await this.federationService.updateInstanceConfiguration(dto); // Audit log for security compliance const auditData: Record = {}; if (dto.name !== undefined) auditData.name = dto.name; if (dto.capabilities !== undefined) auditData.capabilities = dto.capabilities; if (dto.metadata !== undefined) auditData.metadata = dto.metadata; this.auditService.logInstanceConfigurationUpdate(req.user.id, result.instanceId, auditData); return result; } /** * Initiate a connection to a remote instance * Requires authentication */ @Post("connections/initiate") @UseGuards(AuthGuard) async initiateConnection( @Req() req: AuthenticatedRequest, @Body() dto: InitiateConnectionDto ): Promise { if (!req.user?.workspaceId) { throw new Error("Workspace ID not found in request"); } this.logger.log( `User ${req.user.id} initiating connection to ${dto.remoteUrl} for workspace ${req.user.workspaceId}` ); return this.connectionService.initiateConnection(req.user.workspaceId, dto.remoteUrl); } /** * Accept a pending connection * Requires authentication */ @Post("connections/:id/accept") @UseGuards(AuthGuard) async acceptConnection( @Req() req: AuthenticatedRequest, @Param("id") connectionId: string, @Body() dto: AcceptConnectionDto ): Promise { if (!req.user?.workspaceId) { throw new Error("Workspace ID not found in request"); } this.logger.log( `User ${req.user.id} accepting connection ${connectionId} for workspace ${req.user.workspaceId}` ); return this.connectionService.acceptConnection( req.user.workspaceId, connectionId, dto.metadata ); } /** * Reject a pending connection * Requires authentication */ @Post("connections/:id/reject") @UseGuards(AuthGuard) async rejectConnection( @Req() req: AuthenticatedRequest, @Param("id") connectionId: string, @Body() dto: RejectConnectionDto ): Promise { if (!req.user?.workspaceId) { throw new Error("Workspace ID not found in request"); } this.logger.log(`User ${req.user.id} rejecting connection ${connectionId}: ${dto.reason}`); return this.connectionService.rejectConnection(req.user.workspaceId, connectionId, dto.reason); } /** * Disconnect an active connection * Requires authentication */ @Post("connections/:id/disconnect") @UseGuards(AuthGuard) async disconnectConnection( @Req() req: AuthenticatedRequest, @Param("id") connectionId: string, @Body() dto: DisconnectConnectionDto ): Promise { if (!req.user?.workspaceId) { throw new Error("Workspace ID not found in request"); } this.logger.log(`User ${req.user.id} disconnecting connection ${connectionId}`); return this.connectionService.disconnect(req.user.workspaceId, connectionId, dto.reason); } /** * Get all connections for the workspace * Requires authentication */ @Get("connections") @UseGuards(AuthGuard) async getConnections( @Req() req: AuthenticatedRequest, @Query("status") status?: FederationConnectionStatus ): Promise { if (!req.user?.workspaceId) { throw new Error("Workspace ID not found in request"); } return this.connectionService.getConnections(req.user.workspaceId, status); } /** * Get a single connection * Requires authentication */ @Get("connections/:id") @UseGuards(AuthGuard) async getConnection( @Req() req: AuthenticatedRequest, @Param("id") connectionId: string ): Promise { if (!req.user?.workspaceId) { throw new Error("Workspace ID not found in request"); } return this.connectionService.getConnection(req.user.workspaceId, connectionId); } /** * Handle incoming connection request from remote instance * Public endpoint - no authentication required (signature-based verification) */ @Post("incoming/connect") async handleIncomingConnection( @Body() dto: IncomingConnectionRequestDto ): Promise<{ status: string; connectionId?: string }> { this.logger.log(`Received connection request from ${dto.instanceId}`); // LIMITATION: Incoming connections are created in a default workspace // TODO: Future enhancement - Allow configuration of which workspace handles incoming connections // This could be based on routing rules, instance configuration, or a dedicated federation workspace // For now, uses DEFAULT_WORKSPACE_ID environment variable or falls back to "default" const workspaceId = process.env.DEFAULT_WORKSPACE_ID ?? "default"; const connection = await this.connectionService.handleIncomingConnectionRequest( workspaceId, dto ); return { status: "pending", connectionId: connection.id, }; } /** * Spawn an agent on a remote federated instance * Requires authentication */ @Post("agents/spawn") @UseGuards(AuthGuard) async spawnAgentOnRemote( @Req() req: AuthenticatedRequest, @Body() body: { connectionId: string; payload: SpawnAgentCommandPayload } ): Promise { if (!req.user?.workspaceId) { throw new Error("Workspace ID not found in request"); } this.logger.log( `User ${req.user.id} spawning agent on remote instance via connection ${body.connectionId}` ); return this.federationAgentService.spawnAgentOnRemote( req.user.workspaceId, body.connectionId, body.payload ); } /** * Get agent status from remote instance * Requires authentication */ @Get("agents/:agentId/status") @UseGuards(AuthGuard) async getAgentStatus( @Req() req: AuthenticatedRequest, @Param("agentId") agentId: string, @Query("connectionId") connectionId: string ): Promise { if (!req.user?.workspaceId) { throw new Error("Workspace ID not found in request"); } if (!connectionId) { throw new Error("connectionId query parameter is required"); } this.logger.log( `User ${req.user.id} getting agent ${agentId} status via connection ${connectionId}` ); return this.federationAgentService.getAgentStatus(req.user.workspaceId, connectionId, agentId); } /** * Kill an agent on remote instance * Requires authentication */ @Post("agents/:agentId/kill") @UseGuards(AuthGuard) async killAgentOnRemote( @Req() req: AuthenticatedRequest, @Param("agentId") agentId: string, @Body() body: { connectionId: string } ): Promise { if (!req.user?.workspaceId) { throw new Error("Workspace ID not found in request"); } this.logger.log( `User ${req.user.id} killing agent ${agentId} via connection ${body.connectionId}` ); return this.federationAgentService.killAgentOnRemote( req.user.workspaceId, body.connectionId, agentId ); } }