Skip to content

API Integration

Eric Fitzgerald edited this page Nov 12, 2025 · 1 revision

API Integration

This guide provides comprehensive information for integrating with TMI's REST API, WebSocket API, and building client applications.

Table of Contents

REST API Integration

API Overview

TMI provides a RESTful API with OpenAPI 3.0 specification.

Base URL: http://localhost:8080 (development) or https://api.tmi.dev (production)

API Specification: /docs/reference/apis/tmi-openapi.json

Content Type: application/json

Authentication: Bearer token (JWT)

Quick Start

# Get server info (no auth required)
curl http://localhost:8080/

# Get threat models (requires auth)
curl -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  http://localhost:8080/threat_models

Core Endpoints

Threat Models

List Threat Models:

GET /threat_models
Authorization: Bearer {token}

Response:

[
  {
    "id": "uuid",
    "name": "Web Application Security",
    "description": "Security analysis for web app",
    "owner": "alice@example.com",
    "created_at": "2025-01-15T10:00:00Z",
    "modified_at": "2025-01-15T15:30:00Z",
    "diagram_count": 3,
    "threat_count": 12,
    "document_count": 2
  }
]

Create Threat Model:

POST /threat_models
Authorization: Bearer {token}
Content-Type: application/json

{
  "name": "My Threat Model",
  "description": "Security analysis for new feature"
}

Important: Do NOT include calculated or server-controlled fields:

  • id (server-generated)
  • created_at, modified_at (server-set)
  • owner, created_by (from JWT token)
  • diagram_count, threat_count, document_count (calculated)

Update Threat Model:

PUT /threat_models/{threat_model_id}
Authorization: Bearer {token}
Content-Type: application/json

{
  "name": "Updated Name",
  "description": "Updated description"
}

Patch Threat Model:

PATCH /threat_models/{threat_model_id}
Authorization: Bearer {token}
Content-Type: application/json-patch+json

[
  {
    "op": "replace",
    "path": "/name",
    "value": "New Name"
  }
]

Delete Threat Model:

DELETE /threat_models/{threat_model_id}
Authorization: Bearer {token}

Diagrams

Create Diagram:

POST /threat_models/{threat_model_id}/diagrams
Authorization: Bearer {token}
Content-Type: application/json

{
  "name": "System Architecture",
  "description": "High-level data flow diagram"
}

Get Diagram:

GET /threat_models/{threat_model_id}/diagrams/{diagram_id}
Authorization: Bearer {token}

Response:

{
  "id": "uuid",
  "threat_model_id": "uuid",
  "name": "System Architecture",
  "description": "High-level data flow diagram",
  "cells": [
    {
      "id": "cell-uuid",
      "shape": "process",
      "x": 100,
      "y": 200,
      "width": 120,
      "height": 80,
      "label": "Authentication Service"
    }
  ],
  "update_vector": 5
}

Update Diagram Cells:

PUT /diagrams/{diagram_id}/cells
Authorization: Bearer {token}
Content-Type: application/json

{
  "cells": [
    {
      "id": "cell-uuid",
      "shape": "process",
      "x": 150,
      "y": 250,
      "width": 120,
      "height": 80,
      "label": "Updated Label"
    }
  ]
}

Note: Position and size accept both formats:

  • Flat: {x: 100, y: 200, width: 80, height: 60}
  • Nested (legacy): {position: {x: 100, y: 200}, size: {width: 80, height: 60}}

API always returns flat format.

Threats

Create Threat:

POST /threat_models/{threat_model_id}/threats
Authorization: Bearer {token}
Content-Type: application/json

{
  "name": "SQL Injection",
  "description": "Database injection vulnerability",
  "stride": "tampering",
  "severity": "high",
  "status": "open",
  "mitigation": "Use parameterized queries"
}

List Threats:

GET /threat_models/{threat_model_id}/threats
Authorization: Bearer {token}

Metadata

All entities support arbitrary key-value metadata:

Create Metadata:

POST /threat_models/{threat_model_id}/metadata
Authorization: Bearer {token}
Content-Type: application/json

{
  "key": "project_phase",
  "value": "design"
}

List Metadata:

GET /threat_models/{threat_model_id}/metadata
Authorization: Bearer {token}

Update Metadata:

PUT /threat_models/{threat_model_id}/metadata/{key}
Authorization: Bearer {token}
Content-Type: application/json

{
  "value": "implementation"
}

Delete Metadata:

DELETE /threat_models/{threat_model_id}/metadata/{key}
Authorization: Bearer {token}

Authorization Management

Add Authorization Entry:

POST /threat_models/{threat_model_id}/authorization
Authorization: Bearer {token}
Content-Type: application/json

{
  "subject": "bob@example.com",
  "subject_type": "user",
  "role": "writer"
}

Grant Public Read Access:

POST /threat_models/{threat_model_id}/authorization
Authorization: Bearer {token}
Content-Type: application/json

{
  "subject": "everyone",
  "subject_type": "group",
  "role": "reader"
}

Update User Role:

PUT /threat_models/{threat_model_id}/authorization/{index}
Authorization: Bearer {token}
Content-Type: application/json

{
  "role": "owner"
}

Remove Authorization:

DELETE /threat_models/{threat_model_id}/authorization/{index}
Authorization: Bearer {token}

Response Codes

Code Meaning Description
200 OK Successful GET, PUT, PATCH
201 Created Resource created successfully
204 No Content Successful DELETE
400 Bad Request Invalid input data
401 Unauthorized Missing or invalid authentication
403 Forbidden Insufficient permissions
404 Not Found Resource not found
409 Conflict Duplicate resource or state conflict
422 Unprocessable Entity Validation error
500 Internal Server Error Server error

WebSocket API Integration

Overview

TMI provides WebSocket-based real-time collaboration for simultaneous diagram editing.

Connection Flow

1. Join Collaboration Session (REST API)

CRITICAL: Must join via REST API before WebSocket connection.

Create Session (as host):

POST /threat_models/{tm_id}/diagrams/{diagram_id}/collaborate
Authorization: Bearer {token}

Response (201 Created):

{
  "session_id": "uuid",
  "host": "alice@example.com",
  "threat_model_id": "uuid",
  "threat_model_name": "Web App Security",
  "diagram_id": "uuid",
  "diagram_name": "Architecture Diagram",
  "participants": [
    {
      "user_id": "alice@example.com",
      "joined_at": "2025-01-15T10:00:00Z",
      "permissions": "writer"
    }
  ],
  "websocket_url": "ws://localhost:8080/threat_models/.../diagrams/.../ws"
}

Join Existing Session:

PUT /threat_models/{tm_id}/diagrams/{diagram_id}/collaborate
Authorization: Bearer {token}

Response (200 OK): Same structure as create

Check Session Status:

GET /threat_models/{tm_id}/diagrams/{diagram_id}/collaborate
Authorization: Bearer {token}

2. Connect to WebSocket

Use the websocket_url from the session response:

const ws = new WebSocket(`${websocket_url}?token=${jwt_token}`);

ws.onopen = () => {
  console.log('Connected to collaboration session');
};

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

3. Handle Initial State Sync

CRITICAL: First message received is diagram_state_sync:

{
  "message_type": "diagram_state_sync",
  "diagram_id": "uuid",
  "update_vector": 42,
  "cells": [ /* current diagram state */ ]
}

Always handle this message to prevent "cell_already_exists" errors:

function handleDiagramStateSync(message) {
  // Compare with locally cached diagram
  const localVector = cachedDiagram?.update_vector || 0;
  const serverVector = message.update_vector || 0;

  if (serverVector !== localVector) {
    console.warn('State mismatch - resyncing');
    // Update local state with server cells
    cachedDiagram.cells = message.cells;
    cachedDiagram.update_vector = message.update_vector;
    renderDiagram(message.cells);
  }

  isStateSynchronized = true;
}

WebSocket Message Types

Sending Operations

Diagram Operation (cell add/update/remove):

{
  "message_type": "diagram_operation",
  "user_id": "alice@example.com",
  "operation_id": "uuid",
  "operation": {
    "type": "patch",
    "cells": [
      {
        "id": "cell-uuid",
        "operation": "add",
        "data": {
          "shape": "process",
          "x": 100,
          "y": 200,
          "width": 120,
          "height": 80,
          "label": "New Process"
        }
      }
    ]
  }
}

Request Presenter Mode:

{
  "message_type": "presenter_request",
  "user_id": "alice@example.com"
}

Send Cursor Position (only if presenter):

{
  "message_type": "presenter_cursor",
  "user_id": "alice@example.com",
  "cursor_position": { "x": 150, "y": 300 }
}

Undo Request:

{
  "message_type": "undo_request",
  "user_id": "alice@example.com"
}

Redo Request:

{
  "message_type": "redo_request",
  "user_id": "alice@example.com"
}

Receiving Messages

Diagram Operation (from other users):

{
  "message_type": "diagram_operation",
  "user_id": "bob@example.com",
  "operation_id": "uuid",
  "operation": {
    "type": "patch",
    "cells": [ /* changes */ ]
  }
}

Presenter Changed:

{
  "message_type": "current_presenter",
  "current_presenter": "alice@example.com"
}

Presenter Cursor:

{
  "message_type": "presenter_cursor",
  "user_id": "alice@example.com",
  "cursor_position": { "x": 150, "y": 300 }
}

User Joined:

{
  "event": "join",
  "user_id": "charlie@example.com",
  "timestamp": "2025-01-15T10:05:00Z"
}

User Left:

{
  "event": "leave",
  "user_id": "bob@example.com",
  "timestamp": "2025-01-15T10:10:00Z"
}

Session Ended:

{
  "event": "session_ended",
  "user_id": "alice@example.com",
  "message": "Session ended: host has left",
  "timestamp": "2025-01-15T10:15:00Z"
}

State Correction (conflict detected):

{
  "message_type": "state_correction",
  "update_vector": 45
}

Authorization Denied:

{
  "message_type": "authorization_denied",
  "original_operation_id": "uuid",
  "reason": "insufficient_permissions"
}

Echo Prevention

CRITICAL: Never send WebSocket messages when applying remote operations.

class DiagramCollaborationManager {
  constructor(diagramEditor) {
    this.isApplyingRemoteChange = false;

    // Listen to local diagram changes
    this.diagramEditor.on('cellChanged', (change) => {
      if (this.isApplyingRemoteChange) {
        return; // DON'T send WebSocket message
      }
      this.sendOperation(change); // Only send for local changes
    });
  }

  handleDiagramOperation(message) {
    // Skip own operations
    if (message.user_id === this.currentUser.email) {
      return;
    }

    this.isApplyingRemoteChange = true;
    try {
      this.applyOperationToEditor(message.operation);
    } finally {
      this.isApplyingRemoteChange = false;
    }
  }
}

Client Integration Patterns

TypeScript/JavaScript Client

class TMIClient {
  private apiUrl: string;
  private token: string;

  constructor(apiUrl: string, token: string) {
    this.apiUrl = apiUrl;
    this.token = token;
  }

  // REST API Methods
  async getThreatModels(): Promise<ThreatModel[]> {
    const response = await fetch(`${this.apiUrl}/threat_models`, {
      headers: { Authorization: `Bearer ${this.token}` }
    });
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response.json();
  }

  async createThreatModel(data: CreateThreatModelRequest): Promise<ThreatModel> {
    const response = await fetch(`${this.apiUrl}/threat_models`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${this.token}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(data)
    });
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response.json();
  }

  // WebSocket Collaboration
  async startCollaboration(tmId: string, diagramId: string): Promise<WebSocket> {
    // 1. Join session via REST API
    const session = await this.joinCollaborationSession(tmId, diagramId);

    // 2. Connect to WebSocket
    const ws = new WebSocket(`${session.websocket_url}?token=${this.token}`);

    // 3. Set up handlers
    ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      this.handleWebSocketMessage(message);
    };

    return ws;
  }

  private async joinCollaborationSession(tmId: string, diagramId: string) {
    const response = await fetch(
      `${this.apiUrl}/threat_models/${tmId}/diagrams/${diagramId}/collaborate`,
      {
        method: 'POST',
        headers: { Authorization: `Bearer ${this.token}` }
      }
    );

    if (response.status === 409) {
      // Session exists, join instead
      return this.joinExistingSession(tmId, diagramId);
    }

    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response.json();
  }
}

Python Client

import requests
import websocket
import json

class TMIClient:
    def __init__(self, api_url, token):
        self.api_url = api_url
        self.token = token
        self.session = requests.Session()
        self.session.headers.update({
            'Authorization': f'Bearer {token}'
        })

    def get_threat_models(self):
        response = self.session.get(f'{self.api_url}/threat_models')
        response.raise_for_status()
        return response.json()

    def create_threat_model(self, name, description):
        response = self.session.post(
            f'{self.api_url}/threat_models',
            json={'name': name, 'description': description}
        )
        response.raise_for_status()
        return response.json()

    def start_collaboration(self, tm_id, diagram_id):
        # Join session
        session = self.join_collaboration_session(tm_id, diagram_id)

        # Connect to WebSocket
        ws = websocket.WebSocketApp(
            f"{session['websocket_url']}?token={self.token}",
            on_message=self.on_websocket_message,
            on_open=self.on_websocket_open
        )

        return ws

    def on_websocket_message(self, ws, message):
        data = json.loads(message)
        if data['message_type'] == 'diagram_state_sync':
            self.handle_state_sync(data)
        elif data['message_type'] == 'diagram_operation':
            self.handle_diagram_operation(data)

Authentication Integration

OAuth Flow Integration

1. Initiate OAuth

GET /oauth2/authorize?idp=google

Server redirects to Google OAuth, then back to TMI with authorization code.

2. Handle Callback

TMI processes the callback and returns tokens:

GET /oauth2/callback?code=AUTH_CODE&state=STATE

Response:

{
  "access_token": "eyJhbGc...",
  "token_type": "Bearer",
  "expires_in": 86400
}

3. Use Token

Include in Authorization header for all API requests:

Authorization: Bearer eyJhbGc...

Token Management

Get User Info:

GET /oauth2/userinfo
Authorization: Bearer {token}

Response:

{
  "sub": "google:123456789",
  "email": "alice@example.com",
  "name": "Alice Smith",
  "idp": "google"
}

Logout:

POST /oauth2/logout
Authorization: Bearer {token}

Test Provider (Development Only)

For development, use the test OAuth provider:

# Random test user
curl "http://localhost:8080/oauth2/authorize?idp=test"

# Specific test user
curl "http://localhost:8080/oauth2/authorize?idp=test&login_hint=alice"

Error Handling

REST API Errors

All errors return JSON with consistent structure:

{
  "error": "validation_error",
  "message": "Invalid input data",
  "details": {
    "field": "name",
    "reason": "Field is required"
  }
}

WebSocket Errors

Authorization Denied:

{
  "message_type": "authorization_denied",
  "original_operation_id": "uuid",
  "reason": "insufficient_permissions"
}

State Correction:

{
  "message_type": "state_correction",
  "update_vector": 45
}

Handle by re-fetching diagram via REST API:

async function handleStateCorrection(message) {
  console.warn('State correction received, resyncing...');

  const diagram = await fetch(
    `/threat_models/${tmId}/diagrams/${diagramId}`,
    { headers: { Authorization: `Bearer ${token}` } }
  ).then(r => r.json());

  cachedDiagram = diagram;
  renderDiagram(diagram.cells);
}

Retry Logic

async function apiCallWithRetry(url, options, maxRetries = 3) {
  let lastError;

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);

      if (response.ok) return response;

      // Don't retry client errors (4xx except 429)
      if (response.status >= 400 && response.status < 500 && response.status !== 429) {
        throw new Error(`HTTP ${response.status}`);
      }

      // Retry server errors and rate limits
      if (attempt < maxRetries) {
        const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    } catch (error) {
      lastError = error;
      if (attempt < maxRetries) {
        const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }
  }

  throw new Error(`Failed after ${maxRetries} attempts: ${lastError.message}`);
}

Best Practices

1. Authentication

  • Store tokens securely: Use httpOnly cookies or secure storage
  • Refresh tokens: Implement token refresh before expiration
  • Handle 401: Redirect to login on authentication failure
  • Logout properly: Call logout endpoint to invalidate tokens

2. REST API

  • Use appropriate methods: GET for reads, POST for creates, PUT/PATCH for updates
  • Handle errors: Check response status and parse error messages
  • Validate input: Client-side validation before sending to API
  • Paginate large lists: Use pagination parameters when available
  • Don't send calculated fields: Let server compute counts and timestamps

3. WebSocket Collaboration

  • Join via REST first: Always join collaboration session via REST API before WebSocket
  • Handle state sync: Process initial diagram_state_sync message
  • Prevent echo: Don't send WebSocket messages for remote changes
  • Graceful reconnection: Implement exponential backoff for reconnection
  • Update progress: Send progress updates for presenter mode
  • Handle disconnection: Save state before disconnecting

4. Performance

  • Throttle high-frequency events: Throttle cursor updates (100ms), debounce selection (250ms)
  • Batch operations: Use batch endpoints when creating multiple items
  • Cache responses: Cache GET responses with appropriate TTL
  • Use WebSockets wisely: Only for real-time collaboration, not for all updates

5. Error Handling

  • Retry transient errors: Implement exponential backoff for 500/503 errors
  • Don't retry 4xx: Client errors should not be retried
  • Show user-friendly messages: Parse error details and show helpful messages
  • Log errors: Log errors for debugging but don't expose sensitive info to users

6. Security

  • Validate on client and server: Never trust client-side validation alone
  • Sanitize user input: Prevent XSS and injection attacks
  • Use HTTPS: Always use TLS in production
  • Check permissions: Verify user has required role before operations

Code Examples

See /docs/developer/integration/ in the TMI repository for complete code examples:

  • client-integration-guide.md - Comprehensive WebSocket integration
  • client-oauth-integration.md - OAuth client patterns
  • collaborative-editing-plan.md - Real-time editing implementation

API Reference

For complete API documentation:

  • OpenAPI Spec: /docs/reference/apis/tmi-openapi.json
  • WebSocket Spec: /docs/reference/apis/tmi-asyncapi.yml
  • Server Endpoint: http://localhost:8080/ (server info and version)

Next Steps

Clone this wiki locally