Skip to main content

Overview

Jean uses token-based authentication to secure HTTP and WebSocket connections. Authentication can be optionally enabled/disabled via server configuration.

Token Generation

Tokens are cryptographically random 32-byte strings encoded with base64url:
fn generate_token() -> String {
    let mut bytes = [0u8; 32];
    rand::thread_rng().fill(&mut bytes);
    base64::Engine::encode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, bytes)
}
Example token: Kj8vN2Qw1xYzM4P7R6Sf9T3Uc0Vd5We8Xf1Yg4Zh7Ai2Bj

Server Configuration

Authentication is configured when starting the HTTP server:
token
string
required
The authentication token (generated automatically)
token_required
boolean
default:true
Whether authentication is enforced. Set to false to disable authentication.

Authentication Methods

HTTP Query Parameter

Provide the token as a query parameter:
curl "http://127.0.0.1:8420/api/init?token=YOUR_TOKEN"

WebSocket Query Parameter

Include the token when upgrading to WebSocket:
const ws = new WebSocket('ws://127.0.0.1:8420/ws?token=YOUR_TOKEN');

Endpoints

Validate Token

curl "http://127.0.0.1:8420/api/auth?token=YOUR_TOKEN"
ok
boolean
required
true if the token is valid, false otherwise
token_required
boolean
Whether authentication is enabled on the server. Only present when token_required = false.
error
string
Error message when token validation fails. Only present on failure.
Success Response (200)
{
  "ok": true
}
Success Response (No Auth Required)
{
  "ok": true,
  "token_required": false
}
Error Response (401)
{
  "ok": false,
  "error": "Invalid token"
}

WebSocket Authentication

Connection Flow

  1. Upgrade Request: Client sends HTTP GET to /ws?token=YOUR_TOKEN
  2. Validation: Server validates the token (if token_required = true)
  3. Upgrade: On success, connection is upgraded to WebSocket
  4. Event Stream: Client receives real-time events

JavaScript Example

class JeanAPI {
  constructor(baseUrl, token) {
    this.baseUrl = baseUrl;
    this.token = token;
    this.ws = null;
    this.pending = new Map();
  }

  async connect() {
    return new Promise((resolve, reject) => {
      const wsUrl = this.baseUrl.replace('http', 'ws');
      this.ws = new WebSocket(`${wsUrl}/ws?token=${this.token}`);

      this.ws.onopen = () => {
        console.log('Connected to Jean API');
        resolve();
      };

      this.ws.onerror = (error) => {
        console.error('WebSocket error:', error);
        reject(error);
      };

      this.ws.onmessage = (event) => {
        const message = JSON.parse(event.data);
        this.handleMessage(message);
      };

      this.ws.onclose = () => {
        console.log('Disconnected from Jean API');
      };
    });
  }

  handleMessage(message) {
    if (message.type === 'event') {
      // Handle real-time event
      this.emit(message.event, message.payload);
    } else if (message.type === 'response' || message.type === 'error') {
      // Handle command response
      const callback = this.pending.get(message.id);
      if (callback) {
        this.pending.delete(message.id);
        if (message.type === 'response') {
          callback.resolve(message.data);
        } else {
          callback.reject(new Error(message.error));
        }
      }
    }
  }

  async invoke(command, args = {}) {
    const id = crypto.randomUUID();
    return new Promise((resolve, reject) => {
      this.pending.set(id, { resolve, reject });
      this.ws.send(JSON.stringify({ id, command, args }));
    });
  }
}

// Usage
const api = new JeanAPI('http://127.0.0.1:8420', 'YOUR_TOKEN');
await api.connect();

// Now you can invoke commands
const projects = await api.invoke('list_projects');

Python Example

import asyncio
import websockets
import json
import uuid

class JeanAPI:
    def __init__(self, base_url, token):
        self.base_url = base_url
        self.token = token
        self.ws = None
        self.pending = {}
        self.event_handlers = {}

    async def connect(self):
        ws_url = self.base_url.replace('http', 'ws')
        self.ws = await websockets.connect(f'{ws_url}/ws?token={self.token}')
        asyncio.create_task(self._receive_loop())

    async def _receive_loop(self):
        async for message in self.ws:
            data = json.loads(message)
            if data['type'] == 'event':
                handler = self.event_handlers.get(data['event'])
                if handler:
                    handler(data['payload'])
            elif data['type'] in ('response', 'error'):
                future = self.pending.pop(data['id'], None)
                if future:
                    if data['type'] == 'response':
                        future.set_result(data['data'])
                    else:
                        future.set_exception(Exception(data['error']))

    async def invoke(self, command, args=None):
        request_id = str(uuid.uuid4())
        future = asyncio.Future()
        self.pending[request_id] = future
        await self.ws.send(json.dumps({
            'id': request_id,
            'command': command,
            'args': args or {}
        }))
        return await future

    def on(self, event_name, handler):
        self.event_handlers[event_name] = handler

# Usage
async def main():
    api = JeanAPI('http://127.0.0.1:8420', 'YOUR_TOKEN')
    await api.connect()
    
    # Listen for events
    api.on('project:updated', lambda payload: print('Project updated:', payload))
    
    # Invoke commands
    projects = await api.invoke('list_projects')
    print(f'Found {len(projects)} projects')

asyncio.run(main())

Token Validation

The server uses constant-time comparison to prevent timing attacks:
fn validate_token(provided: &str, expected: &str) -> bool {
    if provided.len() != expected.len() {
        return false;
    }
    provided
        .as_bytes()
        .iter()
        .zip(expected.as_bytes().iter())
        .fold(0u8, |acc, (a, b)| acc | (a ^ b))
        == 0
}

Security Considerations

The API currently uses unencrypted HTTP/WebSocket. For production deployments:
  • Use a reverse proxy with TLS (nginx, Caddy)
  • Enable localhost_only mode for local development
  • Consider implementing HTTPS support in the server
  • Store tokens securely (environment variables, secure config files)
  • Never commit tokens to version control
  • Rotate tokens periodically
  • Use different tokens for different environments
When localhost_only = false, the server is accessible on the LAN:
  • Ensure firewall rules are configured properly
  • Use strong, random tokens
  • Consider IP allowlisting
Setting token_required = false disables all authentication:
  • Only use in trusted environments
  • Never expose to the internet without authentication
  • Useful for internal tools and testing

Error Codes

StatusDescription
200 OKToken is valid or authentication is disabled
401 UnauthorizedToken is missing, empty, or invalid
500 Internal Server ErrorServer-side error during validation

Next Steps

API Overview

Learn about the API architecture

Projects API

Start managing projects

Build docs developers (and LLMs) love