fix(#85): resolve TypeScript compilation and validation issues

- Fix @IsNumber() validator on timestamp field (was @IsString() - critical security issue)
- Fix TypeScript compilation error in sortObjectKeys array handling
- Replace generic Error with UnauthorizedException and ServiceUnavailableException
- Document hardcoded workspace ID limitation in handleIncomingConnection
- Remove unused BadRequestException import

All tests passing (70/70), TypeScript compiles cleanly, linting passes.
This commit is contained in:
Jason Woltje
2026-02-03 11:48:23 -06:00
parent fc3919012f
commit df2086ffe8
4 changed files with 35 additions and 25 deletions

View File

@@ -4,7 +4,13 @@
* Manages federation connections between instances. * Manages federation connections between instances.
*/ */
import { Injectable, Logger, NotFoundException } from "@nestjs/common"; import {
Injectable,
Logger,
NotFoundException,
UnauthorizedException,
ServiceUnavailableException,
} from "@nestjs/common";
import { HttpService } from "@nestjs/axios"; import { HttpService } from "@nestjs/axios";
import { FederationConnectionStatus, Prisma } from "@prisma/client"; import { FederationConnectionStatus, Prisma } from "@prisma/client";
import { PrismaService } from "../prisma/prisma.service"; import { PrismaService } from "../prisma/prisma.service";
@@ -247,7 +253,7 @@ export class ConnectionService {
if (!validation.valid) { if (!validation.valid) {
const errorMsg = validation.error ?? "Unknown error"; const errorMsg = validation.error ?? "Unknown error";
this.logger.warn(`Invalid connection request from ${request.instanceId}: ${errorMsg}`); this.logger.warn(`Invalid connection request from ${request.instanceId}: ${errorMsg}`);
throw new Error("Invalid connection request signature"); throw new UnauthorizedException("Invalid connection request signature");
} }
// Create pending connection // Create pending connection
@@ -284,7 +290,9 @@ export class ConnectionService {
} catch (error: unknown) { } catch (error: unknown) {
this.logger.error(`Failed to fetch remote identity from ${remoteUrl}`, error); this.logger.error(`Failed to fetch remote identity from ${remoteUrl}`, error);
const errorMessage = error instanceof Error ? error.message : "Unknown error"; const errorMessage = error instanceof Error ? error.message : "Unknown error";
throw new Error(`Could not connect to remote instance: ${remoteUrl}: ${errorMessage}`); throw new ServiceUnavailableException(
`Could not connect to remote instance: ${remoteUrl}: ${errorMessage}`
);
} }
} }

View File

@@ -4,7 +4,7 @@
* Data Transfer Objects for federation connection API. * Data Transfer Objects for federation connection API.
*/ */
import { IsString, IsUrl, IsOptional, IsObject } from "class-validator"; import { IsString, IsUrl, IsOptional, IsObject, IsNumber } from "class-validator";
/** /**
* DTO for initiating a connection * DTO for initiating a connection
@@ -56,7 +56,7 @@ export class IncomingConnectionRequestDto {
@IsObject() @IsObject()
capabilities!: Record<string, unknown>; capabilities!: Record<string, unknown>;
@IsString() @IsNumber()
timestamp!: number; timestamp!: number;
@IsString() @IsString()

View File

@@ -195,8 +195,10 @@ export class FederationController {
): Promise<{ status: string; connectionId?: string }> { ): Promise<{ status: string; connectionId?: string }> {
this.logger.log(`Received connection request from ${dto.instanceId}`); this.logger.log(`Received connection request from ${dto.instanceId}`);
// For now, create connection in default workspace // LIMITATION: Incoming connections are created in a default workspace
// TODO: Allow configuration of which workspace handles incoming connections // 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 workspaceId = process.env.DEFAULT_WORKSPACE_ID ?? "default";
const connection = await this.connectionService.handleIncomingConnectionRequest( const connection = await this.connectionService.handleIncomingConnectionRequest(

View File

@@ -156,23 +156,22 @@ export class SignatureService {
* @returns A new object with sorted keys * @returns A new object with sorted keys
*/ */
private sortObjectKeys(obj: SignableMessage): SignableMessage { private sortObjectKeys(obj: SignableMessage): SignableMessage {
// Handle null // Handle null and primitives
if (obj === null) {
return obj;
}
// Handle arrays - map recursively
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (Array.isArray(obj)) { if (obj === null || typeof obj !== "object") {
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-argument return obj;
return obj.map((item: any) =>
typeof item === "object" && item !== null ? this.sortObjectKeys(item) : item
) as SignableMessage;
} }
// Handle non-objects (primitives) // Handle arrays - recursively sort elements
if (typeof obj !== "object") { if (Array.isArray(obj)) {
return obj; const sortedArray = obj.map((item: unknown) => {
if (typeof item === "object" && item !== null) {
return this.sortObjectKeys(item as SignableMessage);
}
return item;
});
// Arrays are valid SignableMessage values when nested in objects
return sortedArray as unknown as SignableMessage;
} }
// Handle objects - sort keys alphabetically // Handle objects - sort keys alphabetically
@@ -181,10 +180,11 @@ export class SignatureService {
for (const key of keys) { for (const key of keys) {
const value = obj[key]; const value = obj[key];
sorted[key] = if (typeof value === "object" && value !== null) {
typeof value === "object" && value !== null sorted[key] = this.sortObjectKeys(value as SignableMessage);
? this.sortObjectKeys(value as SignableMessage) } else {
: value; sorted[key] = value;
}
} }
return sorted; return sorted;