feat(#84): implement instance identity model for federation
Implemented the foundation of federation architecture with instance identity and connection management: Database Schema: - Added Instance model for instance identity with keypair generation - Added FederationConnection model for workspace-scoped connections - Added FederationConnectionStatus enum (PENDING, ACTIVE, SUSPENDED, DISCONNECTED) Service Layer: - FederationService with instance identity management - RSA 2048-bit keypair generation for signing - Public identity endpoint (excludes private key) - Keypair regeneration capability API Endpoints: - GET /api/v1/federation/instance - Returns public instance identity - POST /api/v1/federation/instance/regenerate-keys - Admin keypair regeneration Tests: - 11 tests passing (7 service, 4 controller) - 100% statement coverage, 100% function coverage - Follows TDD principles (Red-Green-Refactor) Configuration: - Added INSTANCE_NAME and INSTANCE_URL environment variables - Integrated FederationModule into AppModule Refs #84 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
157
apps/api/src/federation/federation.service.ts
Normal file
157
apps/api/src/federation/federation.service.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Federation Service
|
||||
*
|
||||
* Manages instance identity and federation connections.
|
||||
*/
|
||||
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { Instance, Prisma } from "@prisma/client";
|
||||
import { generateKeyPairSync } from "crypto";
|
||||
import { randomUUID } from "crypto";
|
||||
import { PrismaService } from "../prisma/prisma.service";
|
||||
import {
|
||||
InstanceIdentity,
|
||||
PublicInstanceIdentity,
|
||||
KeyPair,
|
||||
FederationCapabilities,
|
||||
} from "./types/instance.types";
|
||||
|
||||
@Injectable()
|
||||
export class FederationService {
|
||||
private readonly logger = new Logger(FederationService.name);
|
||||
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly config: ConfigService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the instance identity, creating it if it doesn't exist
|
||||
*/
|
||||
async getInstanceIdentity(): Promise<InstanceIdentity> {
|
||||
// Try to find existing instance
|
||||
let instance = await this.prisma.instance.findFirst();
|
||||
|
||||
if (!instance) {
|
||||
this.logger.log("No instance identity found, creating new one");
|
||||
instance = await this.createInstanceIdentity();
|
||||
}
|
||||
|
||||
return this.mapToInstanceIdentity(instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get public instance identity (without private key)
|
||||
*/
|
||||
async getPublicIdentity(): Promise<PublicInstanceIdentity> {
|
||||
const instance = await this.getInstanceIdentity();
|
||||
|
||||
// Exclude private key from public identity
|
||||
const { privateKey: _privateKey, ...publicIdentity } = instance;
|
||||
|
||||
return publicIdentity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new RSA key pair for instance signing
|
||||
*/
|
||||
generateKeypair(): KeyPair {
|
||||
const { publicKey, privateKey } = generateKeyPairSync("rsa", {
|
||||
modulusLength: 2048,
|
||||
publicKeyEncoding: {
|
||||
type: "spki",
|
||||
format: "pem",
|
||||
},
|
||||
privateKeyEncoding: {
|
||||
type: "pkcs8",
|
||||
format: "pem",
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
publicKey,
|
||||
privateKey,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerate the instance's keypair
|
||||
*/
|
||||
async regenerateKeypair(): Promise<InstanceIdentity> {
|
||||
const instance = await this.getInstanceIdentity();
|
||||
const { publicKey, privateKey } = this.generateKeypair();
|
||||
|
||||
const updatedInstance = await this.prisma.instance.update({
|
||||
where: { id: instance.id },
|
||||
data: {
|
||||
publicKey,
|
||||
privateKey,
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.log("Instance keypair regenerated");
|
||||
|
||||
return this.mapToInstanceIdentity(updatedInstance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance identity
|
||||
*/
|
||||
private async createInstanceIdentity(): Promise<Instance> {
|
||||
const { publicKey, privateKey } = this.generateKeypair();
|
||||
|
||||
const instanceId = this.generateInstanceId();
|
||||
const name = this.config.get<string>("INSTANCE_NAME") ?? "Mosaic Instance";
|
||||
const url = this.config.get<string>("INSTANCE_URL") ?? "http://localhost:3000";
|
||||
|
||||
const capabilities: FederationCapabilities = {
|
||||
supportsQuery: true,
|
||||
supportsCommand: true,
|
||||
supportsEvent: true,
|
||||
supportsAgentSpawn: true,
|
||||
protocolVersion: "1.0",
|
||||
};
|
||||
|
||||
const instance = await this.prisma.instance.create({
|
||||
data: {
|
||||
instanceId,
|
||||
name,
|
||||
url,
|
||||
publicKey,
|
||||
privateKey,
|
||||
capabilities: capabilities as Prisma.JsonObject,
|
||||
metadata: {},
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.log(`Created instance identity: ${instanceId}`);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique instance ID
|
||||
*/
|
||||
private generateInstanceId(): string {
|
||||
return `instance-${randomUUID()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Prisma Instance to InstanceIdentity type
|
||||
*/
|
||||
private mapToInstanceIdentity(instance: Instance): InstanceIdentity {
|
||||
return {
|
||||
id: instance.id,
|
||||
instanceId: instance.instanceId,
|
||||
name: instance.name,
|
||||
url: instance.url,
|
||||
publicKey: instance.publicKey,
|
||||
privateKey: instance.privateKey,
|
||||
capabilities: instance.capabilities as FederationCapabilities,
|
||||
metadata: instance.metadata as Record<string, unknown>,
|
||||
createdAt: instance.createdAt,
|
||||
updatedAt: instance.updatedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user