# Kosmi WebSocket Authentication - Reverse Engineering Findings

**Date**: October 31, 2025  
**Status**: ✅ Authentication mechanism fully reverse engineered

## Summary

The Kosmi WebSocket API (`wss://engine.kosmi.io/gql-ws`) requires a JWT token that is obtained via an HTTP POST request. The token is then sent in the `connection_init` message payload.

## Authentication Flow

```
1. Browser visits: https://app.kosmi.io/room/@roomname
   ↓
2. JavaScript makes POST to: https://engine.kosmi.io/
   ↓
3. Server returns JWT token (valid for 1 year!)
   ↓
4. JavaScript connects WebSocket: wss://engine.kosmi.io/gql-ws
   ↓
5. Sends connection_init with token
   ↓
6. Server responds with connection_ack
   ↓
7. Ready to send GraphQL subscriptions/mutations
```

## Key Findings

### 1. JWT Token Structure

The token is a standard JWT with the following payload:

```json
{
  "aud": "kosmi",
  "exp": 1793367309,
  "iat": 1761917709,
  "iss": "kosmi",
  "jti": "c824a175-46e6-4ffc-b69a-42f319d62460",
  "nbf": 1761917708,
  "sub": "a067ec32-ad5c-4831-95cc-0f88bdb33587",
  "typ": "access"
}
```

**Important fields**:
- `sub`: Anonymous user ID (UUID)
- `exp`: Expiration timestamp (1 year from issuance!)
- `typ`: "access" token type

### 2. Token Acquisition

**Request**:
```http
POST https://engine.kosmi.io/
Content-Type: application/json
Referer: https://app.kosmi.io/
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
```

**Body**: (Likely a GraphQL query/mutation for anonymous login or session creation)

**Response**: Returns JWT token (details to be captured in next test)

### 3. WebSocket Connection Init

**WebSocket URL**: `wss://engine.kosmi.io/gql-ws`

**Protocol**: `graphql-ws`

**First message (connection_init)**:
```json
{
  "type": "connection_init",
  "payload": {
    "token": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9...",
    "ua": "TW96aWxsYS81LjAgKE1hY2ludG9zaDsgSW50ZWwgTWFjIE9TIFggMTBfMTVfNykgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzEyMC4wLjAuMCBTYWZhcmkvNTM3LjM2",
    "v": "4364",
    "r": ""
  }
}
```

**Payload fields**:
- `token`: The JWT access token
- `ua`: Base64-encoded User-Agent string
- `v`: Version number "4364" (app version?)
- `r`: Empty string (possibly room-related, unused for anonymous)

**Response**:
```json
{
  "type": "connection_ack",
  "payload": {}
}
```

### 4. Required Headers

**For POST request**:
```go
headers := http.Header{
    "Content-Type": []string{"application/json"},
    "Referer": []string{"https://app.kosmi.io/"},
    "User-Agent": []string{"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"},
}
```

**For WebSocket**:
```go
headers := http.Header{
    "Origin": []string{"https://app.kosmi.io"},
    "User-Agent": []string{"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"},
}

dialer := websocket.Dialer{
    Subprotocols: []string{"graphql-ws"},
}
```

### 5. Cookies

**Cookie found**: `g_state`

**Value structure**:
```json
{
  "i_l": 0,
  "i_ll": 1761917710911,
  "i_b": "w4+5eCKfslo5DMEmBzdtPYztYGoOkFIbwBzrc4xEzDk"
}
```

**Purpose**: Appears to be Google Analytics state or similar tracking.

**Required for WebSocket?**: ❌ NO - The JWT token is the primary authentication mechanism.

## Implementation Strategy

### Step 1: Token Acquisition

We need to reverse engineer the POST request body. Two approaches:

**Option A: Capture the POST body**
- Modify capture tool to log request bodies
- See exactly what GraphQL query/mutation is sent

**Option B: Test common patterns**
- Try empty body: `{}`
- Try anonymous login mutation
- Try session creation query

### Step 2: Native Client Implementation

```go
type NativeClient struct {
    httpClient *http.Client
    wsConn     *websocket.Conn
    token      string
    roomID     string
    log        *logrus.Entry
}

func (c *NativeClient) Connect() error {
    // 1. Get JWT token
    token, err := c.acquireToken()
    if err != nil {
        return err
    }
    c.token = token
    
    // 2. Connect WebSocket
    dialer := websocket.Dialer{
        Subprotocols: []string{"graphql-ws"},
    }
    
    headers := http.Header{
        "Origin": []string{"https://app.kosmi.io"},
        "User-Agent": []string{userAgent},
    }
    
    conn, _, err := dialer.Dial("wss://engine.kosmi.io/gql-ws", headers)
    if err != nil {
        return err
    }
    c.wsConn = conn
    
    // 3. Send connection_init
    return c.sendConnectionInit()
}

func (c *NativeClient) acquireToken() (string, error) {
    // POST to https://engine.kosmi.io/
    // Parse response for JWT token
}

func (c *NativeClient) sendConnectionInit() error {
    uaEncoded := base64.StdEncoding.EncodeToString([]byte(userAgent))
    
    msg := map[string]interface{}{
        "type": "connection_init",
        "payload": map[string]interface{}{
            "token": c.token,
            "ua":    uaEncoded,
            "v":     "4364",
            "r":     "",
        },
    }
    
    return c.wsConn.WriteJSON(msg)
}
```

## Next Steps

1. ✅ Capture token acquisition POST body
2. ✅ Implement `acquireToken()` function
3. ✅ Test direct WebSocket connection with token
4. ✅ Verify message subscription works
5. ✅ Verify message sending works
6. ✅ Replace ChromeDP client

## Success Criteria

- [ ] Can obtain JWT token without browser
- [ ] Can connect WebSocket with just token
- [ ] Can receive messages
- [ ] Can send messages  
- [ ] No ChromeDP dependency
- [ ] < 50MB RAM usage
- [ ] < 1 second startup time

## Additional Notes

### Token Validity

The JWT token has a **1-year expiration**! This means:
- We can cache the token
- No need to re-authenticate frequently
- Simplifies the implementation significantly

### Anonymous Access

The Kosmi API supports **true anonymous access**:
- No login credentials needed
- Just POST to get a token
- Token is tied to an anonymous user UUID

This is excellent news for our use case!

### No Cookies Required

Unlike our initial assumption, **cookies are NOT required** for WebSocket authentication. The JWT token in the `connection_init` payload is sufficient.

## References

- Captured data: `auth-data.json`
- Capture tool: `cmd/capture-auth/main.go`
- Chrome extension (reference): `.examples/chrome-extension/inject.js`

