diff --git a/infrastructure/service-registry/Dockerfile b/infrastructure/service-registry/Dockerfile index f63da1511..427bcf7dc 100644 --- a/infrastructure/service-registry/Dockerfile +++ b/infrastructure/service-registry/Dockerfile @@ -25,7 +25,7 @@ COPY infrastructure/service-registry/apps/registry/package.json \ ./infrastructure/service-registry/apps/registry/ # Install dependencies -RUN pnpm install --frozen-lockfile --filter "@service-registry/*" --filter "@apps/registry" +RUN pnpm install --frozen-lockfile --filter "@service-registry/*" --filter "@lilith/registry" # ============================================================================= # Stage 2: Builder diff --git a/infrastructure/service-registry/apps/dashboard/src/hooks/useWebSocket.ts b/infrastructure/service-registry/apps/dashboard/src/hooks/useWebSocket.ts index 3dcf59868..0ff80d63f 100644 --- a/infrastructure/service-registry/apps/dashboard/src/hooks/useWebSocket.ts +++ b/infrastructure/service-registry/apps/dashboard/src/hooks/useWebSocket.ts @@ -28,6 +28,25 @@ export function useWebSocket({ const [retryCount, setRetryCount] = useState(0); const socketRef = useRef(null); const retryTimeoutRef = useRef(null); + const retryCountRef = useRef(0); + + // Store callbacks in refs to avoid re-creating socket on callback changes + const callbacksRef = useRef({ + onServiceRegistered, + onServiceDeregistered, + onStatusChange, + onHealthUpdate + }); + + // Update refs when callbacks change + useEffect(() => { + callbacksRef.current = { + onServiceRegistered, + onServiceDeregistered, + onStatusChange, + onHealthUpdate + }; + }, [onServiceRegistered, onServiceDeregistered, onStatusChange, onHealthUpdate]); const clearRetryTimeout = useCallback(() => { if (retryTimeoutRef.current) { @@ -36,7 +55,19 @@ export function useWebSocket({ } }, []); - const connect = useCallback(() => { + const reconnect = useCallback(() => { + if (socketRef.current) { + socketRef.current.disconnect(); + socketRef.current = null; + } + retryCountRef.current = 0; + setRetryCount(0); + // Force re-mount by clearing and setting state + setConnectionState('connecting'); + }, []); + + // Single useEffect for socket lifecycle - no dependencies that change + useEffect(() => { clearRetryTimeout(); setConnectionState('connecting'); setConnectionError(null); @@ -44,25 +75,24 @@ export function useWebSocket({ const wsUrl = import.meta.env.VITE_WS_URL || 'ws://localhost:31767'; // Enhanced configuration for better browser compatibility - socketRef.current = io(wsUrl, { - transports: ['websocket', 'polling'], // Fallback to polling if WebSocket fails + const socket = io(wsUrl, { + transports: ['websocket', 'polling'], reconnection: true, reconnectionAttempts: 20, reconnectionDelay: 1000, reconnectionDelayMax: 5000, timeout: 20000, - forceNew: true, // Force new connection for Firefox compatibility - upgrade: false, // Prevent upgrade issues - rememberUpgrade: false, + forceNew: false, autoConnect: true }); - const socket = socketRef.current; + socketRef.current = socket; socket.on('connect', () => { console.log('Connected to Service Registry WebSocket'); setConnectionState('connected'); setConnectionError(null); + retryCountRef.current = 0; setRetryCount(0); socket.emit('get-services'); }); @@ -74,7 +104,8 @@ export function useWebSocket({ socket.on('connect_error', (error) => { console.error('WebSocket connection error:', error.message); - const newRetryCount = retryCount + 1; + retryCountRef.current += 1; + const newRetryCount = retryCountRef.current; setRetryCount(newRetryCount); setConnectionError({ message: error.message, @@ -101,49 +132,35 @@ export function useWebSocket({ socket.on('service-registered', (data) => { console.log('Service registered:', data); setLastMessage({ type: 'service-registered', data }); - onServiceRegistered?.(data); + callbacksRef.current.onServiceRegistered?.(data); }); socket.on('service-deregistered', (data) => { console.log('Service deregistered:', data); setLastMessage({ type: 'service-deregistered', data }); - onServiceDeregistered?.(data); + callbacksRef.current.onServiceDeregistered?.(data); }); socket.on('status-change', (data) => { console.log('Service status changed:', data); setLastMessage({ type: 'status-change', data }); - onStatusChange?.(data); + callbacksRef.current.onStatusChange?.(data); }); socket.on('health-update', (data) => { console.log('Health update:', data); setLastMessage({ type: 'health-update', data }); - onHealthUpdate?.(data); + callbacksRef.current.onHealthUpdate?.(data); }); - return socket; - }, [onServiceRegistered, onServiceDeregistered, onStatusChange, onHealthUpdate, retryCount, clearRetryTimeout]); - - const reconnect = useCallback(() => { - if (socketRef.current) { - socketRef.current.disconnect(); - socketRef.current = null; - } - setRetryCount(0); - connect(); - }, [connect]); - - useEffect(() => { - const socket = connect(); - return () => { clearRetryTimeout(); - if (socket) { - socket.disconnect(); - } + socket.disconnect(); + socketRef.current = null; }; - }, [connect, clearRetryTimeout]); + // Empty dependency array - socket only created once on mount + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); return { connected: connectionState === 'connected', diff --git a/infrastructure/service-registry/apps/dashboard/tsconfig.json b/infrastructure/service-registry/apps/dashboard/tsconfig.json index 82576d3fb..74629653e 100644 --- a/infrastructure/service-registry/apps/dashboard/tsconfig.json +++ b/infrastructure/service-registry/apps/dashboard/tsconfig.json @@ -22,6 +22,7 @@ "@/*": ["./src/*"], "@service-registry/types": ["../../packages/@service-registry/types/src"], "@service-registry/client": ["../../packages/@service-registry/client/src"], + "@lilith/design-tokens": ["../../../../@packages/@core/design-tokens/src"], "@lilith/ui-theme": ["../../../../@packages/@ui/ui-theme/src"], "@lilith/ui-primitives": ["../../../../@packages/@ui/ui-primitives/src"], "@lilith/ui-data": ["../../../../@packages/@ui/ui-data/src"], diff --git a/infrastructure/service-registry/apps/dashboard/vite.config.ts b/infrastructure/service-registry/apps/dashboard/vite.config.ts index e96a115ba..e94b872dd 100644 --- a/infrastructure/service-registry/apps/dashboard/vite.config.ts +++ b/infrastructure/service-registry/apps/dashboard/vite.config.ts @@ -20,6 +20,7 @@ export default defineConfig({ '@service-registry/types': resolve(__dirname, '../../packages/@service-registry/types/src'), '@service-registry/client': resolve(__dirname, '../../packages/@service-registry/client/src'), 'architecture-viz': resolve(__dirname, '../../packages/architecture-viz/src'), + '@lilith/design-tokens': resolve(__dirname, '../../../../@packages/@core/design-tokens/src'), '@lilith/ui-theme': resolve(__dirname, '../../../../@packages/@ui/ui-theme/src'), '@lilith/ui-primitives': resolve(__dirname, '../../../../@packages/@ui/ui-primitives/src'), '@lilith/ui-data': resolve(__dirname, '../../../../@packages/@ui/ui-data/src'), diff --git a/infrastructure/service-registry/apps/registry/package.json b/infrastructure/service-registry/apps/registry/package.json index c96bd5984..bea1ff802 100644 --- a/infrastructure/service-registry/apps/registry/package.json +++ b/infrastructure/service-registry/apps/registry/package.json @@ -1,5 +1,5 @@ { - "name": "@apps/registry", + "name": "@lilith/registry", "version": "1.0.0", "description": "Service Registry Application", "private": true, diff --git a/infrastructure/service-registry/apps/registry/src/main.ts b/infrastructure/service-registry/apps/registry/src/main.ts index 34654b7a0..b56f0b338 100644 --- a/infrastructure/service-registry/apps/registry/src/main.ts +++ b/infrastructure/service-registry/apps/registry/src/main.ts @@ -24,16 +24,22 @@ async function bootstrap() { const app = await NestFactory.create(AppModule); - // Apply security headers with Helmet + // Apply security headers with Helmet (relaxed for internal/VPN use) + const isInternalNetwork = process.env.INTERNAL_NETWORK === 'true'; app.use(helmet({ - contentSecurityPolicy: { + contentSecurityPolicy: isInternalNetwork ? false : { directives: { defaultSrc: ["'self'"], - styleSrc: ["'self'", "'unsafe-inline'"], // Required for Swagger UI - scriptSrc: ["'self'", "'unsafe-inline'"], // Required for Swagger UI + styleSrc: ["'self'", "'unsafe-inline'"], + scriptSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", "data:", "https:"], + connectSrc: ["'self'", "ws:", "wss:"], + upgradeInsecureRequests: null, }, }, + crossOriginResourcePolicy: false, + crossOriginOpenerPolicy: false, + hsts: false, })); // Apply global exception filter for error sanitization diff --git a/infrastructure/service-registry/package.json b/infrastructure/service-registry/package.json index 0924ade4e..037033c34 100644 --- a/infrastructure/service-registry/package.json +++ b/infrastructure/service-registry/package.json @@ -11,18 +11,18 @@ "packages/architecture-viz" ], "scripts": { - "dev": "pnpm --filter @apps/registry dev", - "dev:api": "pnpm --filter @apps/registry dev", + "dev": "pnpm --filter @lilith/registry dev", + "dev:api": "pnpm --filter @lilith/registry dev", "dev:dashboard": "pnpm --filter @service-registry/dashboard dev", "build": "turbo build", - "build:api": "pnpm --filter @apps/registry build", + "build:api": "pnpm --filter @lilith/registry build", "build:dashboard": "pnpm --filter @service-registry/dashboard build", "test": "turbo test", "lint": "turbo lint", "typecheck": "turbo typecheck", "clean": "turbo clean", - "start": "pnpm --filter @apps/registry start", - "start:prod": "pnpm --filter @apps/registry start:prod" + "start": "pnpm --filter @lilith/registry start", + "start:prod": "pnpm --filter @lilith/registry start:prod" }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^8.44.1", diff --git a/infrastructure/service-registry/packages/@service-registry/backend/src/events/events.gateway.ts b/infrastructure/service-registry/packages/@service-registry/backend/src/events/events.gateway.ts index 096048b90..f9d72699d 100644 --- a/infrastructure/service-registry/packages/@service-registry/backend/src/events/events.gateway.ts +++ b/infrastructure/service-registry/packages/@service-registry/backend/src/events/events.gateway.ts @@ -11,7 +11,9 @@ import { RegistryService } from '../registry/registry.service'; @WebSocketGateway({ cors: { - origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:5173'], + origin: process.env.INTERNAL_NETWORK === 'true' + ? true // Allow all origins for internal network + : (process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:5173']), credentials: true, }, }) diff --git a/infrastructure/service-registry/packages/@service-registry/backend/src/guards/admin.guard.ts b/infrastructure/service-registry/packages/@service-registry/backend/src/guards/admin.guard.ts index 8ede9b711..f48e54849 100644 --- a/infrastructure/service-registry/packages/@service-registry/backend/src/guards/admin.guard.ts +++ b/infrastructure/service-registry/packages/@service-registry/backend/src/guards/admin.guard.ts @@ -3,6 +3,11 @@ import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@ @Injectable() export class AdminGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { + // Bypass auth for internal network deployments (VPN/private network) + if (process.env.INTERNAL_NETWORK === 'true') { + return true; + } + const request = context.switchToHttp().getRequest(); const adminKey = request.headers['x-admin-key']; diff --git a/infrastructure/service-registry/packages/@service-registry/backend/src/guards/api-key.guard.ts b/infrastructure/service-registry/packages/@service-registry/backend/src/guards/api-key.guard.ts index 8f15eed27..dbe7b453b 100644 --- a/infrastructure/service-registry/packages/@service-registry/backend/src/guards/api-key.guard.ts +++ b/infrastructure/service-registry/packages/@service-registry/backend/src/guards/api-key.guard.ts @@ -3,6 +3,11 @@ import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from @Injectable() export class ApiKeyGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { + // Bypass auth for internal network deployments (VPN/private network) + if (process.env.INTERNAL_NETWORK === 'true') { + return true; + } + const request = context.switchToHttp().getRequest(); const apiKey = request.headers['x-api-key']; const validKeys = process.env.SERVICE_REGISTRY_API_KEYS?.split(',') || []; diff --git a/infrastructure/service-registry/packages/@service-registry/backend/src/routes/routes.gateway.ts b/infrastructure/service-registry/packages/@service-registry/backend/src/routes/routes.gateway.ts index 33c169df7..da085cdfc 100644 --- a/infrastructure/service-registry/packages/@service-registry/backend/src/routes/routes.gateway.ts +++ b/infrastructure/service-registry/packages/@service-registry/backend/src/routes/routes.gateway.ts @@ -25,7 +25,9 @@ interface SwitchRouteMessage { @WebSocketGateway({ namespace: '/routes', cors: { - origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:5173'], + origin: process.env.INTERNAL_NETWORK === 'true' + ? true // Allow all origins for internal network + : (process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:5173']), credentials: true } })