fix(dash): address safety constraints, validation, and test quality issues
This commit is contained in:
@@ -51,6 +51,11 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
|
||||
}
|
||||
}
|
||||
|
||||
// Configuration constants
|
||||
const MAX_CONNECTIONS = 10;
|
||||
const MAX_PAYLOAD_BYTES = 16384; // 16KB
|
||||
const IDLE_TIMEOUT_SECONDS = 60;
|
||||
|
||||
// Interval for broadcasting stats to all connected WS clients
|
||||
let statsBroadcastInterval: Timer | undefined;
|
||||
|
||||
@@ -62,6 +67,13 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
|
||||
|
||||
// Upgrade to WebSocket
|
||||
if (url.pathname === "/ws") {
|
||||
// Security Check: limit concurrent connections
|
||||
const currentConnections = server.pendingWebSockets;
|
||||
if (currentConnections >= MAX_CONNECTIONS) {
|
||||
console.warn(`⚠️ [WS] Connection rejected: limit reached (${currentConnections}/${MAX_CONNECTIONS})`);
|
||||
return new Response("Connection limit reached", { status: 429 });
|
||||
}
|
||||
|
||||
const success = server.upgrade(req);
|
||||
if (success) return undefined;
|
||||
return new Response("WebSocket upgrade failed", { status: 400 });
|
||||
@@ -137,30 +149,43 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
|
||||
}, 5000);
|
||||
}
|
||||
},
|
||||
message(ws, message) {
|
||||
// Handle messages if needed (e.g. heartbeat)
|
||||
async message(ws, message) {
|
||||
try {
|
||||
const data = JSON.parse(message.toString());
|
||||
if (data.type === "PING") ws.send(JSON.stringify({ type: "PONG" }));
|
||||
} catch (e) { }
|
||||
const { WsMessageSchema } = await import("@shared/modules/dashboard/dashboard.types");
|
||||
const parsed = WsMessageSchema.safeParse(JSON.parse(message.toString()));
|
||||
|
||||
if (!parsed.success) {
|
||||
console.error("❌ [WS] Invalid message format:", parsed.error.issues);
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsed.data.type === "PING") {
|
||||
ws.send(JSON.stringify({ type: "PONG" }));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("❌ [WS] Failed to parse message:", e);
|
||||
}
|
||||
},
|
||||
close(ws) {
|
||||
ws.unsubscribe("dashboard");
|
||||
console.log(`🔌 [WS] Client disconnected.`);
|
||||
console.log(`🔌 [WS] Client disconnected. Total remaining: ${server.pendingWebSockets}`);
|
||||
|
||||
// Stop broadcast interval if no clients left
|
||||
if (server.pendingWebSockets === 0 && statsBroadcastInterval) {
|
||||
clearInterval(statsBroadcastInterval);
|
||||
statsBroadcastInterval = undefined;
|
||||
}
|
||||
}
|
||||
},
|
||||
maxPayloadLength: MAX_PAYLOAD_BYTES,
|
||||
idleTimeout: IDLE_TIMEOUT_SECONDS,
|
||||
},
|
||||
|
||||
development: isDev,
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper to fetch full dashboard stats object
|
||||
* Helper to fetch full dashboard stats object.
|
||||
* Unified for both HTTP API and WebSocket broadcasts.
|
||||
*/
|
||||
async function getFullDashboardStats() {
|
||||
// Import services (dynamic to avoid circular deps)
|
||||
@@ -189,7 +214,7 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
|
||||
},
|
||||
recentEvents: recentEvents.map(event => ({
|
||||
...event,
|
||||
timestamp: event.timestamp.toISOString(),
|
||||
timestamp: event.timestamp instanceof Date ? event.timestamp.toISOString() : event.timestamp,
|
||||
})),
|
||||
uptime: clientStats.uptime,
|
||||
lastCommandTimestamp: clientStats.lastCommandTimestamp,
|
||||
|
||||
Reference in New Issue
Block a user