From 5ae07f7a841da480b42c2f32f0ae05839bd4d9f7 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Thu, 5 Feb 2026 16:55:48 -0600 Subject: [PATCH] fix(#338): Validate DEFAULT_WORKSPACE_ID as UUID - Add federation.config.ts with UUID v4 validation for DEFAULT_WORKSPACE_ID - Validate at module initialization (fail fast if misconfigured) - Replace hardcoded "default" fallback with proper validation - Add 18 tests covering valid UUIDs, invalid formats, and missing values - Clear error messages with expected UUID format Refs #338 Co-Authored-By: Claude Opus 4.5 --- .../src/federation/federation.config.spec.ts | 164 ++++++++++++++++++ apps/api/src/federation/federation.config.ts | 58 +++++++ .../src/federation/federation.controller.ts | 5 +- apps/api/src/federation/federation.module.ts | 24 ++- 4 files changed, 247 insertions(+), 4 deletions(-) create mode 100644 apps/api/src/federation/federation.config.spec.ts create mode 100644 apps/api/src/federation/federation.config.ts diff --git a/apps/api/src/federation/federation.config.spec.ts b/apps/api/src/federation/federation.config.spec.ts new file mode 100644 index 0000000..9a0203e --- /dev/null +++ b/apps/api/src/federation/federation.config.spec.ts @@ -0,0 +1,164 @@ +/** + * Federation Configuration Tests + * + * Issue #338: Tests for DEFAULT_WORKSPACE_ID validation + */ + +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { + isValidUuidV4, + getDefaultWorkspaceId, + validateFederationConfig, +} from "./federation.config"; + +describe("federation.config", () => { + const originalEnv = process.env.DEFAULT_WORKSPACE_ID; + + afterEach(() => { + // Restore original environment + if (originalEnv === undefined) { + delete process.env.DEFAULT_WORKSPACE_ID; + } else { + process.env.DEFAULT_WORKSPACE_ID = originalEnv; + } + }); + + describe("isValidUuidV4", () => { + it("should return true for valid UUID v4", () => { + const validUuids = [ + "123e4567-e89b-42d3-a456-426614174000", + "550e8400-e29b-41d4-a716-446655440000", + "6ba7b810-9dad-41d1-80b4-00c04fd430c8", + "f47ac10b-58cc-4372-a567-0e02b2c3d479", + ]; + + for (const uuid of validUuids) { + expect(isValidUuidV4(uuid)).toBe(true); + } + }); + + it("should return true for uppercase UUID v4", () => { + expect(isValidUuidV4("123E4567-E89B-42D3-A456-426614174000")).toBe(true); + }); + + it("should return false for non-v4 UUID (wrong version digit)", () => { + // UUID v1 (version digit is 1) + expect(isValidUuidV4("123e4567-e89b-12d3-a456-426614174000")).toBe(false); + // UUID v3 (version digit is 3) + expect(isValidUuidV4("123e4567-e89b-32d3-a456-426614174000")).toBe(false); + // UUID v5 (version digit is 5) + expect(isValidUuidV4("123e4567-e89b-52d3-a456-426614174000")).toBe(false); + }); + + it("should return false for invalid variant digit", () => { + // Variant digit should be 8, 9, a, or b + expect(isValidUuidV4("123e4567-e89b-42d3-0456-426614174000")).toBe(false); + expect(isValidUuidV4("123e4567-e89b-42d3-7456-426614174000")).toBe(false); + expect(isValidUuidV4("123e4567-e89b-42d3-c456-426614174000")).toBe(false); + }); + + it("should return false for non-UUID strings", () => { + expect(isValidUuidV4("")).toBe(false); + expect(isValidUuidV4("default")).toBe(false); + expect(isValidUuidV4("not-a-uuid")).toBe(false); + expect(isValidUuidV4("123e4567-e89b-12d3-a456")).toBe(false); + expect(isValidUuidV4("123e4567e89b12d3a456426614174000")).toBe(false); + }); + + it("should return false for UUID with wrong length", () => { + expect(isValidUuidV4("123e4567-e89b-42d3-a456-4266141740001")).toBe(false); + expect(isValidUuidV4("123e4567-e89b-42d3-a456-42661417400")).toBe(false); + }); + }); + + describe("getDefaultWorkspaceId", () => { + it("should return valid UUID when DEFAULT_WORKSPACE_ID is set correctly", () => { + const validUuid = "123e4567-e89b-42d3-a456-426614174000"; + process.env.DEFAULT_WORKSPACE_ID = validUuid; + + expect(getDefaultWorkspaceId()).toBe(validUuid); + }); + + it("should trim whitespace from UUID", () => { + const validUuid = "123e4567-e89b-42d3-a456-426614174000"; + process.env.DEFAULT_WORKSPACE_ID = ` ${validUuid} `; + + expect(getDefaultWorkspaceId()).toBe(validUuid); + }); + + it("should throw error when DEFAULT_WORKSPACE_ID is not set", () => { + delete process.env.DEFAULT_WORKSPACE_ID; + + expect(() => getDefaultWorkspaceId()).toThrow( + "DEFAULT_WORKSPACE_ID environment variable is required for federation but is not set" + ); + }); + + it("should throw error when DEFAULT_WORKSPACE_ID is empty string", () => { + process.env.DEFAULT_WORKSPACE_ID = ""; + + expect(() => getDefaultWorkspaceId()).toThrow( + "DEFAULT_WORKSPACE_ID environment variable is required for federation but is not set" + ); + }); + + it("should throw error when DEFAULT_WORKSPACE_ID is only whitespace", () => { + process.env.DEFAULT_WORKSPACE_ID = " "; + + expect(() => getDefaultWorkspaceId()).toThrow( + "DEFAULT_WORKSPACE_ID environment variable is required for federation but is not set" + ); + }); + + it("should throw error when DEFAULT_WORKSPACE_ID is 'default' (not a valid UUID)", () => { + process.env.DEFAULT_WORKSPACE_ID = "default"; + + expect(() => getDefaultWorkspaceId()).toThrow("DEFAULT_WORKSPACE_ID must be a valid UUID v4"); + expect(() => getDefaultWorkspaceId()).toThrow('Current value "default" is not a valid UUID'); + }); + + it("should throw error when DEFAULT_WORKSPACE_ID is invalid UUID format", () => { + process.env.DEFAULT_WORKSPACE_ID = "not-a-valid-uuid"; + + expect(() => getDefaultWorkspaceId()).toThrow("DEFAULT_WORKSPACE_ID must be a valid UUID v4"); + }); + + it("should throw error for UUID v1 (wrong version)", () => { + process.env.DEFAULT_WORKSPACE_ID = "123e4567-e89b-12d3-a456-426614174000"; + + expect(() => getDefaultWorkspaceId()).toThrow("DEFAULT_WORKSPACE_ID must be a valid UUID v4"); + }); + + it("should include helpful error message with expected format", () => { + process.env.DEFAULT_WORKSPACE_ID = "invalid"; + + expect(() => getDefaultWorkspaceId()).toThrow( + "Expected format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx" + ); + }); + }); + + describe("validateFederationConfig", () => { + it("should not throw when DEFAULT_WORKSPACE_ID is valid", () => { + process.env.DEFAULT_WORKSPACE_ID = "123e4567-e89b-42d3-a456-426614174000"; + + expect(() => validateFederationConfig()).not.toThrow(); + }); + + it("should throw when DEFAULT_WORKSPACE_ID is missing", () => { + delete process.env.DEFAULT_WORKSPACE_ID; + + expect(() => validateFederationConfig()).toThrow( + "DEFAULT_WORKSPACE_ID environment variable is required for federation" + ); + }); + + it("should throw when DEFAULT_WORKSPACE_ID is invalid", () => { + process.env.DEFAULT_WORKSPACE_ID = "invalid-uuid"; + + expect(() => validateFederationConfig()).toThrow( + "DEFAULT_WORKSPACE_ID must be a valid UUID v4" + ); + }); + }); +}); diff --git a/apps/api/src/federation/federation.config.ts b/apps/api/src/federation/federation.config.ts new file mode 100644 index 0000000..8e5b27b --- /dev/null +++ b/apps/api/src/federation/federation.config.ts @@ -0,0 +1,58 @@ +/** + * Federation Configuration + * + * Validates federation-related environment variables at startup. + * Issue #338: Validate DEFAULT_WORKSPACE_ID is a valid UUID + */ + +/** + * UUID v4 regex pattern + * Matches standard UUID format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx + * where y is 8, 9, a, or b + */ +const UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + +/** + * Check if a string is a valid UUID v4 + */ +export function isValidUuidV4(value: string): boolean { + return UUID_V4_REGEX.test(value); +} + +/** + * Get the configured default workspace ID for federation + * @throws Error if DEFAULT_WORKSPACE_ID is not set or is not a valid UUID + */ +export function getDefaultWorkspaceId(): string { + const workspaceId = process.env.DEFAULT_WORKSPACE_ID; + + if (!workspaceId || workspaceId.trim() === "") { + throw new Error( + "DEFAULT_WORKSPACE_ID environment variable is required for federation but is not set. " + + "Please configure a valid UUID v4 workspace ID for handling incoming federation connections." + ); + } + + const trimmedId = workspaceId.trim(); + + if (!isValidUuidV4(trimmedId)) { + throw new Error( + `DEFAULT_WORKSPACE_ID must be a valid UUID v4. ` + + `Current value "${trimmedId}" is not a valid UUID format. ` + + `Expected format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx (where y is 8, 9, a, or b)` + ); + } + + return trimmedId; +} + +/** + * Validates federation configuration at startup. + * Call this during module initialization to fail fast if misconfigured. + * + * @throws Error if DEFAULT_WORKSPACE_ID is not set or is not a valid UUID + */ +export function validateFederationConfig(): void { + // Validate DEFAULT_WORKSPACE_ID - this will throw if invalid + getDefaultWorkspaceId(); +} diff --git a/apps/api/src/federation/federation.controller.ts b/apps/api/src/federation/federation.controller.ts index 1aceb6a..c9b0b1c 100644 --- a/apps/api/src/federation/federation.controller.ts +++ b/apps/api/src/federation/federation.controller.ts @@ -10,6 +10,7 @@ import { Throttle } from "@nestjs/throttler"; import { FederationService } from "./federation.service"; import { FederationAuditService } from "./audit.service"; import { ConnectionService } from "./connection.service"; +import { getDefaultWorkspaceId } from "./federation.config"; import { AuthGuard } from "../auth/guards/auth.guard"; import { AdminGuard } from "../auth/guards/admin.guard"; import { WorkspaceGuard } from "../common/guards/workspace.guard"; @@ -225,8 +226,8 @@ export class FederationController { // 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"; + // Issue #338: Validate DEFAULT_WORKSPACE_ID is a valid UUID (throws if invalid/missing) + const workspaceId = getDefaultWorkspaceId(); const connection = await this.connectionService.handleIncomingConnectionRequest( workspaceId, diff --git a/apps/api/src/federation/federation.module.ts b/apps/api/src/federation/federation.module.ts index d146631..1e8e5b2 100644 --- a/apps/api/src/federation/federation.module.ts +++ b/apps/api/src/federation/federation.module.ts @@ -3,9 +3,10 @@ * * Provides instance identity and federation management with DoS protection via rate limiting. * Issue #272: Rate limiting added to prevent DoS attacks on federation endpoints + * Issue #338: Validate DEFAULT_WORKSPACE_ID at startup */ -import { Module } from "@nestjs/common"; +import { Module, Logger, OnModuleInit } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; import { HttpModule } from "@nestjs/axios"; import { ThrottlerModule } from "@nestjs/throttler"; @@ -20,6 +21,7 @@ import { OIDCService } from "./oidc.service"; import { CommandService } from "./command.service"; import { QueryService } from "./query.service"; import { FederationAgentService } from "./federation-agent.service"; +import { validateFederationConfig } from "./federation.config"; import { PrismaModule } from "../prisma/prisma.module"; import { TasksModule } from "../tasks/tasks.module"; import { EventsModule } from "../events/events.module"; @@ -83,4 +85,22 @@ import { RedisProvider } from "../common/providers/redis.provider"; FederationAgentService, ], }) -export class FederationModule {} +export class FederationModule implements OnModuleInit { + private readonly logger = new Logger(FederationModule.name); + + /** + * Validate federation configuration at module initialization. + * Issue #338: Fail fast if DEFAULT_WORKSPACE_ID is not a valid UUID. + */ + onModuleInit(): void { + try { + validateFederationConfig(); + this.logger.log("Federation configuration validated successfully"); + } catch (error) { + this.logger.error( + `Federation configuration validation failed: ${error instanceof Error ? error.message : String(error)}` + ); + throw error; + } + } +}