Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -53,19 +53,15 @@ def __init__(
ws_url: WebSocket URL (e.g., "ws://127.0.0.1:8000/ws")
on_message: Callback function called when a message is received (optional,
can be set later via set_callback)
api_key: Optional API key for authentication (sent as query parameter)
api_key: Optional API key for authentication (sent in X-Api-Key header)
"""
if not WEBSOCKETS_AVAILABLE:
logger.warning("websockets library not available, real-time sync disabled")

# Append API key as query parameter if provided
if api_key:
separator = "&" if "?" in ws_url else "?"
self.ws_url = f"{ws_url}{separator}token={api_key}"
else:
self.ws_url = ws_url
self.ws_url = ws_url
self.on_message = on_message
self.api_key = api_key
self._extra_headers = {"X-Api-Key": api_key} if api_key else {}
self._websocket: Any = None
self._state = ConnectionState.DISCONNECTED
self._lock = asyncio.Lock()
Expand Down Expand Up @@ -173,7 +169,10 @@ async def _run(self) -> None:

while self._state != ConnectionState.DISCONNECTED:
try:
async with websockets.connect(self.ws_url) as websocket: # type: ignore[attr-defined]
connect_kwargs = {}
if self._extra_headers:
connect_kwargs["extra_headers"] = self._extra_headers
async with websockets.connect(self.ws_url, **connect_kwargs) as websocket: # type: ignore[attr-defined]
self._websocket = websocket
async with self._lock:
# State may have changed to DISCONNECTED during connection
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,22 @@ async def websocket_endpoint(
websocket: WebSocket,
manager: ConnectionManagerWsDep,
server_config: ServerConfigWsDep,
token: str | None = Query(None, description="API key for authentication"),
token: str | None = Query(None, description="API key for authentication (deprecated: use X-Api-Key header)"),
) -> None:
"""WebSocket endpoint for real-time task updates.

Clients connect to this endpoint to receive real-time notifications
when tasks are created, updated, or deleted.

Authentication:
Pass API key as query parameter: /ws?token=sk-xxx
Pass API key as HTTP header: X-Api-Key: sk-xxx (preferred)
or query parameter: /ws?token=sk-xxx (deprecated fallback)

Args:
websocket: The WebSocket connection
manager: Connection manager dependency
server_config: Server configuration dependency
token: API key for authentication (query parameter)
token: API key for authentication (query parameter fallback)

Message Format:
{
Expand All @@ -46,9 +47,12 @@ async def websocket_endpoint(
"data": {...} # Full task data or relevant fields
}
"""
# Accept API key from X-Api-Key header or query parameter fallback
api_key = websocket.headers.get("x-api-key") or token

# Validate API key before accepting connection
try:
client_name = validate_api_key_for_websocket(token, server_config)
client_name = validate_api_key_for_websocket(api_key, server_config)
except ValueError as e:
# Use standard WebSocket close code 1008 (Policy Violation) for auth failures
await websocket.close(code=1008, reason=str(e))
Expand Down
Loading