# Agent Signaling Server Integration Guide

This guide explains how external agents and automated clients can interface with the Cloudflare Worker signaling server (`worker.js`) to establish real-time peer coordination and WebRTC data channels.

The signaling server utilizes **Cloudflare Durable Objects** (`RoomDO`) to coordinate clients grouped under arbitrary logical rooms. All signaling is performed in-memory on the server, guaranteeing latency-free dispatching and strong consistency.

---

## Technical Overview

The signaling protocol consists of two main communications channels:
1. **Server-Sent Events (SSE)** (`GET /api/connect`): A persistent unidirectional stream from the server to the client. This stream delivers authentication status, room roster updates, and incoming signaling payloads.
2. **REST Endpoints** (`POST /api/signal` and `POST /api/leave`): Standard HTTP POST endpoints used by the client to send outbound signaling payloads or explicitly leave the coordination room.

### Code References
*   **Signaling Server Code:** [worker.js](file:///c:/Users/asus/Projects/web-projects/signalling-webRTC/worker.js)
*   **WebRTC Client Implementation:** [p2pClient.ts](file:///c:/Users/asus/Projects/web-projects/pair-drop/src/helpers/p2pClient.ts)

---

## 1. Connection & Session Initialization

To join a room, a client initiates an SSE connection to `/api/connect`.

### Endpoint Detail
*   **Method:** `GET`
*   **Path:** `/api/connect`
*   **Query Parameters:**
    *   `room` (Required): The name of the coordination room. The server normalizes this to uppercase and trims whitespaces (e.g. `room=agent-room`).
    *   `session_id` (Optional): An existing client UUID. Used for session reconnection.
    *   `token` (Optional): The secure UUID token previously assigned to the `session_id`. Required if `session_id` is passed.

### Initial Server Response
The server responds with a standard `text/event-stream` headers block and starts streaming SSE events.

```http
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache, no-transform
Connection: keep-alive
```

---

## 2. Server-Sent Events (SSE) Protocol

Once connected, the server emits three custom event types: `joined`, `roster`, and `signal`, along with a periodic keepalive comment.

### Event Type: `joined`
Sent immediately upon a successful new connection. It provides the client with its unique assigned ID, a private session token for authenticating subsequent requests, and dynamic TURN credentials.

*   **Format:** `event: joined\ndata: <JSON_STRING>\n\n`
*   **JSON Payload Schema:**
    ```json
    {
      "id": "c1f7ab2d-056e-44d5-8df6-9de1c9693cfc",
      "token": "fa72b530-9b48-4cb2-8356-11f8e1215b22",
      "iceServers": [
        {
          "urls": [
            "stun:openrelay.metered.ca:80",
            "turn:openrelay.metered.ca:80?transport=udp"
          ],
          "username": "...",
          "credential": "..."
        }
      ]
    }
    ```

> [Spacer Alert]
> [!IMPORTANT]
> The private `token` must be kept secret. All future signaling and leave requests sent by this client must include this token. If they do not, the server will reject them with a `403 Forbidden` response.

### Event Type: `roster`
Sent to all clients in the room whenever a peer joins or leaves. It provides a complete list of all currently active peer IDs.

*   **Format:** `event: roster\ndata: <JSON_STRING>\n\n`
*   **JSON Payload Schema:**
    ```json
    [
      "c1f7ab2d-056e-44d5-8df6-9de1c9693cfc",
      "8e19e99a-7a54-4361-b58e-324d142ab115"
    ]
    ```

### Event Type: `signal`
Delivered when another peer sends a signaling message to this client.

*   **Format:** `event: signal\ndata: <JSON_STRING>\n\n`
*   **JSON Payload Schema:**
    ```json
    {
      "sender": "8e19e99a-7a54-4361-b58e-324d142ab115",
      "signal": {
        "sdp": {
          "type": "offer",
          "sdp": "v=0\no=- 279532... \n..."
        },
        "connectionMode": "lan"
      }
    }
    ```

### SSE Heartbeat / Keepalive
To keep intermediate network proxies from closing idle connections, the server enqueues a keepalive comment every 10 seconds:
```http
: keepalive


```

---

## 3. Outbound Signaling Dispatch

Clients send signaling data to targeted peers using a POST request. The server validates the request in-memory and enqueues the payload directly onto the recipient's SSE stream.

### Endpoint Detail
*   **Method:** `POST`
*   **Path:** `/api/signal`
*   **Query Parameters:**
    *   `room` (Required): The room name.
*   **JSON Request Body:**
    ```json
    {
      "sender": "c1f7ab2d-056e-44d5-8df6-9de1c9693cfc",
      "to": "8e19e99a-7a54-4361-b58e-324d142ab115",
      "token": "fa72b530-9b48-4cb2-8356-11f8e1215b22",
      "signal": {
        "sdp": { ... },
        "candidate": { ... }
      }
    }
    ```
*   **Response:**
    *   `200 OK` on success: `{"success": true}`
    *   `400 Bad Request` if missing parameters: `{"error": "Missing required parameters"}`
    *   `403 Forbidden` if unauthorized: `{"error": "Unauthorized: invalid session token"}`
    *   `429 Too Many Requests` if rate limit is exceeded: `{"error": "Too Many Requests: signaling rate limit exceeded"}` (Note: Senders are rate limited to a burst capacity of 50 signals with a refill rate of 10 tokens/second).

---

## 4. Lifecycle & Session Reconnection

### Session Pruning
The server scans sessions every 30 seconds. If a client's SSE connection disconnects, the server keeps the session in memory. However, if no heartbeat or activity is registered for **90 seconds**, the session is aggressively pruned, and other peers receive an updated `roster`.

### Reconnection Protocol
If an agent temporarily drops its network connection, it can reconnect to the same session without generating a new UUID peer identity. 

To reconnect, append the previously received `id` as `session_id` and `token` as `token` to the `/api/connect` request:
```
GET /api/connect?room=AGENT-ROOM&session_id=c1f7ab2d-056e-44d5-8df6-9de1c9693cfc&token=fa72b530-9b48-4cb2-8356-11f8e1215b22
```

---

## Agent Integration Examples

Below are complete, ready-to-run clients showing how agents can connect and exchange messages.

### Python Agent Client

This Python implementation uses `httpx` to handle the asynchronous Server-Sent Events stream and HTTP POST requests.

```python
import asyncio
import json
import httpx

SIGNAL_SERVER_URL = "http://localhost:3000"  # Update with your deployment URL
ROOM_NAME = "AGENT-COORDINATION"

class AgentSignalingClient:
    def __init__(self, server_url: str, room: str):
        self.server_url = server_url
        self.room = room
        self.client_id = None
        self.token = None
        self.ice_servers = None
        self.roster = []
        self.http_client = httpx.AsyncClient()

    async def connect(self):
        url = f"{self.server_url}/api/connect?room={self.room}"
        print(f"Connecting to room '{self.room}' SSE stream...")
        
        async with self.http_client.stream("GET", url, timeout=None) as response:
            if response.status_code != 200:
                print(f"Failed to connect: {response.status_code}")
                return

            # SSE line-by-line parser
            event_type = None
            async for line in response.iter_lines():
                if not line.strip():
                    continue
                
                if line.startswith("event:"):
                    event_type = line.split(":", 1)[1].strip()
                elif line.startswith("data:"):
                    data_str = line.split(":", 1)[1].strip()
                    await self.handle_event(event_type, data_str)
                    event_type = None

    async def handle_event(self, event_type: str, data_str: str):
        data = json.loads(data_str)
        if event_type == "joined":
            self.client_id = data["id"]
            self.token = data["token"]
            self.ice_servers = data.get("iceServers")
            print(f"[Joined] Assigned Peer ID: {self.client_id}")
            print(f"[Joined] Private Token: {self.token}")
            
        elif event_type == "roster":
            self.roster = data
            print(f"[Roster Update] Active peers: {self.roster}")
            
        elif event_type == "signal":
            sender = data["sender"]
            signal = data["signal"]
            print(f"[Signal Received] From {sender}: {signal}")
            await self.on_signal(sender, signal)

    async def on_signal(self, sender: str, signal: dict):
        # Implement your agent decision logic / WebRTC handshaking here
        # Example: Respond with a greeting or acknowledge the SDP
        pass

    async def send_signal(self, target_id: str, signal_payload: dict):
        if not self.client_id or not self.token:
            print("Cannot send signal: Not authenticated yet.")
            return

        url = f"{self.server_url}/api/signal?room={self.room}"
        payload = {
            "sender": self.client_id,
            "to": target_id,
            "token": self.token,
            "signal": signal_payload
        }
        
        res = await self.http_client.post(url, json=payload)
        if res.status_code == 200:
            print(f"[Signal Sent] Successfully sent payload to {target_id}")
        else:
            print(f"[Error] Failed to send signal: {res.text}")

    async def leave(self):
        if not self.client_id or not self.token:
            return
        url = f"{self.server_url}/api/leave?room={self.room}"
        await self.http_client.post(url, json={"sender": self.client_id, "token": self.token})
        print("Left the room.")
        await self.http_client.aclose()

async def main():
    client = AgentSignalingClient(SIGNAL_SERVER_URL, ROOM_NAME)
    
    # Run the SSE connection in a background task
    connection_task = asyncio.create_task(client.connect())
    
    # Wait for the client to register and retrieve its ID
    while not client.client_id:
        await asyncio.sleep(0.5)

    # Example: Broadcast a custom hello ping to the first available peer
    await asyncio.sleep(2)
    other_peers = [p for p in client.roster if p != client.client_id]
    if other_peers:
        target = other_peers[0]
        await client.send_signal(target, {"type": "agent-ping", "message": "Hello from python agent!"})

    # Keep running for 30s then disconnect gracefully
    await asyncio.sleep(30)
    await client.leave()
    connection_task.cancel()

if __name__ == "__main__":
    asyncio.run(main())
```

---

### Node.js Agent Client

This Node.js implementation uses standard `EventSource` (or `eventsource` library on older node versions) and `fetch`.

```javascript
import EventSource from 'eventsource';

const SIGNAL_SERVER_URL = "http://localhost:3000";
const ROOM_NAME = "AGENT-COORDINATION";

class NodeAgentClient {
  constructor(serverUrl, room) {
    this.serverUrl = serverUrl;
    this.room = room;
    this.clientId = null;
    this.token = null;
    this.iceServers = null;
    this.roster = [];
    this.es = null;
  }

  connect() {
    const url = `${this.serverUrl}/api/connect?room=${this.room}`;
    console.log(`Connecting to ${url}...`);

    this.es = new EventSource(url);

    this.es.addEventListener('joined', (e) => {
      const data = JSON.parse(e.data);
      this.clientId = data.id;
      this.token = data.token;
      this.iceServers = data.iceServers;
      console.log(`[Joined] Assigned ID: ${this.clientId}`);
    });

    this.es.addEventListener('roster', (e) => {
      this.roster = JSON.parse(e.data);
      console.log(`[Roster Update] Active peers:`, this.roster);
    });

    this.es.addEventListener('signal', (e) => {
      const { sender, signal } = JSON.parse(e.data);
      console.log(`[Signal Received] From ${sender}:`, signal);
      this.onSignal(sender, signal);
    });

    this.es.onerror = (err) => {
      console.error('[Signaling Connection Error]', err);
    };
  }

  onSignal(sender, signal) {
    // Implement your WebRTC handler or custom protocol here
  }

  async sendSignal(targetId, signalPayload) {
    if (!this.clientId || !this.token) {
      console.warn("Cannot send signal: client not registered.");
      return;
    }

    const url = `${this.serverUrl}/api/signal?room=${this.room}`;
    try {
      const response = await fetch(url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          sender: this.clientId,
          to: targetId,
          token: this.token,
          signal: signalPayload
        })
      });
      const data = await response.json();
      if (data.success) {
        console.log(`[Signal Sent] Dispatched payload to ${targetId}`);
      } else {
        console.error(`[Error] Failed to send signal:`, data.error);
      }
    } catch (err) {
      console.error('Network error sending signal:', err);
    }
  }

  async leave() {
    if (!this.clientId || !this.token) return;
    const url = `${this.serverUrl}/api/leave?room=${this.room}`;
    try {
      await fetch(url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ sender: this.clientId, token: this.token })
      });
      console.log("Left room successfully.");
    } catch (e) {
      console.error("Error leaving room:", e);
    } finally {
      if (this.es) {
        this.es.close();
      }
    }
  }
}

// Quick Execution Mock
const agent = new NodeAgentClient(SIGNAL_SERVER_URL, ROOM_NAME);
agent.connect();

// Clean up on exit
process.on('SIGINT', async () => {
  console.log('\nGracefully exiting...');
  await agent.leave();
  process.exit(0);
});
```
