feat(#292): implement protocol version checking

Add protocol version validation during connection handshake.
- Define FEDERATION_PROTOCOL_VERSION constant (1.0)
- Validate version on both outgoing and incoming connections
- Require exact version match for compatibility
- Log and audit version mismatches

Fixes #292

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-03 22:00:16 -06:00
parent d373ce591f
commit 14ae97bba4
3 changed files with 115 additions and 0 deletions

View File

@@ -150,6 +150,31 @@ describe("ConnectionService", () => {
);
});
it("should reject connection to instance with incompatible protocol version", async () => {
const incompatibleRemoteIdentity = {
...mockRemoteIdentity,
capabilities: {
...mockRemoteIdentity.capabilities,
protocolVersion: "2.0",
},
};
const mockAxiosResponse: AxiosResponse = {
data: incompatibleRemoteIdentity,
status: 200,
statusText: "OK",
headers: {},
config: {} as never,
};
vi.spyOn(prismaService.federationConnection, "count").mockResolvedValue(5);
vi.spyOn(httpService, "get").mockReturnValue(of(mockAxiosResponse));
await expect(service.initiateConnection(mockWorkspaceId, mockRemoteUrl)).rejects.toThrow(
"Incompatible protocol version. Expected 1.0, received 2.0"
);
});
it("should create a pending connection", async () => {
const mockAxiosResponse: AxiosResponse = {
data: mockRemoteIdentity,
@@ -449,6 +474,42 @@ describe("ConnectionService", () => {
signature: "valid-signature",
};
it("should reject request with incompatible protocol version", async () => {
const incompatibleRequest = {
...mockRequest,
capabilities: {
...mockRemoteIdentity.capabilities,
protocolVersion: "2.0",
},
};
vi.spyOn(signatureService, "verifyConnectionRequest").mockResolvedValue({ valid: true });
await expect(
service.handleIncomingConnectionRequest(mockWorkspaceId, incompatibleRequest)
).rejects.toThrow("Incompatible protocol version. Expected 1.0, received 2.0");
});
it("should accept request with compatible protocol version", async () => {
const compatibleRequest = {
...mockRequest,
capabilities: {
...mockRemoteIdentity.capabilities,
protocolVersion: "1.0",
},
};
vi.spyOn(signatureService, "verifyConnectionRequest").mockResolvedValue({ valid: true });
vi.spyOn(prismaService.federationConnection, "create").mockResolvedValue(mockConnection);
const result = await service.handleIncomingConnectionRequest(
mockWorkspaceId,
compatibleRequest
);
expect(result.status).toBe(FederationConnectionStatus.PENDING);
});
it("should validate connection request signature", async () => {
const verifySpy = vi.spyOn(signatureService, "verifyConnectionRequest");
vi.spyOn(prismaService.federationConnection, "create").mockResolvedValue(mockConnection);

View File

@@ -21,6 +21,7 @@ import { FederationAuditService } from "./audit.service";
import { firstValueFrom } from "rxjs";
import type { ConnectionRequest, ConnectionDetails } from "./types/connection.types";
import type { PublicInstanceIdentity } from "./types/instance.types";
import { FEDERATION_PROTOCOL_VERSION } from "./constants";
@Injectable()
export class ConnectionService {
@@ -55,6 +56,9 @@ export class ConnectionService {
// Fetch remote instance identity
const remoteIdentity = await this.fetchRemoteIdentity(remoteUrl);
// Validate protocol version compatibility
this.validateProtocolVersion(remoteIdentity.capabilities.protocolVersion);
// Get our instance identity
const localIdentity = await this.federationService.getInstanceIdentity();
@@ -316,6 +320,25 @@ export class ConnectionService {
throw new UnauthorizedException("Invalid connection request signature");
}
// Validate protocol version compatibility
try {
this.validateProtocolVersion(request.capabilities.protocolVersion);
} catch (error) {
const errorMsg = error instanceof Error ? error.message : "Unknown error";
this.logger.warn(`Incompatible protocol version from ${request.instanceId}: ${errorMsg}`);
// Audit log: Connection rejected
this.auditService.logIncomingConnectionRejected({
workspaceId,
remoteInstanceId: request.instanceId,
remoteUrl: request.instanceUrl,
reason: "Incompatible protocol version",
error: errorMsg,
});
throw error;
}
// Create pending connection
const connection = await this.prisma.federationConnection.create({
data: {
@@ -403,4 +426,22 @@ export class ConnectionService {
disconnectedAt: connection.disconnectedAt,
};
}
/**
* Validate protocol version compatibility
* Currently requires exact version match
*/
private validateProtocolVersion(remoteVersion: string | undefined): void {
if (!remoteVersion) {
throw new BadRequestException(
`Protocol version not specified. Expected ${FEDERATION_PROTOCOL_VERSION}`
);
}
if (remoteVersion !== FEDERATION_PROTOCOL_VERSION) {
throw new BadRequestException(
`Incompatible protocol version. Expected ${FEDERATION_PROTOCOL_VERSION}, received ${remoteVersion}`
);
}
}
}

View File

@@ -0,0 +1,13 @@
/**
* Federation Protocol Constants
*
* Constants for federation protocol versioning and configuration.
*/
/**
* Current federation protocol version
* Format: MAJOR.MINOR
* - MAJOR version: Breaking changes to protocol
* - MINOR version: Backward-compatible additions
*/
export const FEDERATION_PROTOCOL_VERSION = "1.0";