fix(#338): Enforce WSS in production and add connect_error handling

- Add validateWebSocketSecurity() to warn when using ws:// in production
- Add connect_error event handler to capture connection failures
- Expose connectionError state to consumers via hook and provider
- Add comprehensive tests for WSS enforcement and error handling

Refs #338

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jason Woltje
2026-02-05 17:31:26 -06:00
parent 63a622cbef
commit dd46025d60
4 changed files with 194 additions and 2 deletions

View File

@@ -31,9 +31,32 @@ interface WebSocketCallbacks {
onProjectUpdated?: (project: Project) => void;
}
interface ConnectionError {
message: string;
type: string;
description?: string;
}
interface UseWebSocketReturn {
isConnected: boolean;
socket: Socket | null;
connectionError: ConnectionError | null;
}
/**
* Check if the WebSocket URL uses secure protocol (wss://)
* Logs a warning in production when using insecure ws://
*/
function validateWebSocketSecurity(url: string): void {
const isProduction = process.env.NODE_ENV === "production";
const isSecure = url.startsWith("https://") || url.startsWith("wss://");
if (isProduction && !isSecure) {
console.warn(
"[Security Warning] WebSocket connection using insecure protocol (ws://). " +
"Authentication tokens may be exposed. Use wss:// in production."
);
}
}
/**
@@ -42,7 +65,7 @@ interface UseWebSocketReturn {
* @param workspaceId - The workspace ID to subscribe to
* @param token - Authentication token
* @param callbacks - Event callbacks for real-time updates
* @returns Connection status and socket instance
* @returns Connection status, socket instance, and connection error
*/
export function useWebSocket(
workspaceId: string,
@@ -51,6 +74,7 @@ export function useWebSocket(
): UseWebSocketReturn {
const [socket, setSocket] = useState<Socket | null>(null);
const [isConnected, setIsConnected] = useState<boolean>(false);
const [connectionError, setConnectionError] = useState<ConnectionError | null>(null);
const {
onTaskCreated,
@@ -66,6 +90,12 @@ export function useWebSocket(
// Get WebSocket URL from environment or default to API URL
const wsUrl = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3001";
// Validate WebSocket security - warn if using insecure connection in production
validateWebSocketSecurity(wsUrl);
// Clear any previous connection error
setConnectionError(null);
// Create socket connection
const newSocket = io(wsUrl, {
auth: { token },
@@ -83,8 +113,18 @@ export function useWebSocket(
setIsConnected(false);
};
const handleConnectError = (error: Error): void => {
setConnectionError({
message: error.message || "Connection failed",
type: "connect_error",
description: "Failed to establish WebSocket connection",
});
setIsConnected(false);
};
newSocket.on("connect", handleConnect);
newSocket.on("disconnect", handleDisconnect);
newSocket.on("connect_error", handleConnectError);
// Real-time event handlers
if (onTaskCreated) {
@@ -113,6 +153,7 @@ export function useWebSocket(
return (): void => {
newSocket.off("connect", handleConnect);
newSocket.off("disconnect", handleDisconnect);
newSocket.off("connect_error", handleConnectError);
if (onTaskCreated) newSocket.off("task:created", onTaskCreated);
if (onTaskUpdated) newSocket.off("task:updated", onTaskUpdated);
@@ -139,5 +180,6 @@ export function useWebSocket(
return {
isConnected,
socket,
connectionError,
};
}