# 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](#rest-api-integration) - [WebSocket API Integration](#websocket-api-integration) - [Client Integration Patterns](#client-integration-patterns) - [Authentication Integration](#authentication-integration) - [Error Handling](#error-handling) - [Best Practices](#best-practices) ## 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 ```bash # 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**: ```http GET /threat_models Authorization: Bearer {token} ``` Response: ```json [ { "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**: ```http 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**: ```http PUT /threat_models/{threat_model_id} Authorization: Bearer {token} Content-Type: application/json { "name": "Updated Name", "description": "Updated description" } ``` **Patch Threat Model**: ```http 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**: ```http DELETE /threat_models/{threat_model_id} Authorization: Bearer {token} ``` #### Diagrams **Create Diagram**: ```http 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**: ```http GET /threat_models/{threat_model_id}/diagrams/{diagram_id} Authorization: Bearer {token} ``` Response: ```json { "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**: ```http 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**: ```http 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**: ```http GET /threat_models/{threat_model_id}/threats Authorization: Bearer {token} ``` #### Metadata All entities support arbitrary key-value metadata: **Create Metadata**: ```http POST /threat_models/{threat_model_id}/metadata Authorization: Bearer {token} Content-Type: application/json { "key": "project_phase", "value": "design" } ``` **List Metadata**: ```http GET /threat_models/{threat_model_id}/metadata Authorization: Bearer {token} ``` **Update Metadata**: ```http PUT /threat_models/{threat_model_id}/metadata/{key} Authorization: Bearer {token} Content-Type: application/json { "value": "implementation" } ``` **Delete Metadata**: ```http DELETE /threat_models/{threat_model_id}/metadata/{key} Authorization: Bearer {token} ``` ### Authorization Management **Add Authorization Entry**: ```http 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**: ```http POST /threat_models/{threat_model_id}/authorization Authorization: Bearer {token} Content-Type: application/json { "subject": "everyone", "subject_type": "group", "role": "reader" } ``` **Update User Role**: ```http PUT /threat_models/{threat_model_id}/authorization/{index} Authorization: Bearer {token} Content-Type: application/json { "role": "owner" } ``` **Remove Authorization**: ```http 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)**: ```http POST /threat_models/{tm_id}/diagrams/{diagram_id}/collaborate Authorization: Bearer {token} ``` Response (201 Created): ```json { "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**: ```http PUT /threat_models/{tm_id}/diagrams/{diagram_id}/collaborate Authorization: Bearer {token} ``` Response (200 OK): Same structure as create **Check Session Status**: ```http GET /threat_models/{tm_id}/diagrams/{diagram_id}/collaborate Authorization: Bearer {token} ``` #### 2. Connect to WebSocket Use the `websocket_url` from the session response: ```javascript 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`: ```json { "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: ```javascript 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): ```json { "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**: ```json { "message_type": "presenter_request", "user_id": "alice@example.com" } ``` **Send Cursor Position** (only if presenter): ```json { "message_type": "presenter_cursor", "user_id": "alice@example.com", "cursor_position": { "x": 150, "y": 300 } } ``` **Undo Request**: ```json { "message_type": "undo_request", "user_id": "alice@example.com" } ``` **Redo Request**: ```json { "message_type": "redo_request", "user_id": "alice@example.com" } ``` #### Receiving Messages **Diagram Operation** (from other users): ```json { "message_type": "diagram_operation", "user_id": "bob@example.com", "operation_id": "uuid", "operation": { "type": "patch", "cells": [ /* changes */ ] } } ``` **Presenter Changed**: ```json { "message_type": "current_presenter", "current_presenter": "alice@example.com" } ``` **Presenter Cursor**: ```json { "message_type": "presenter_cursor", "user_id": "alice@example.com", "cursor_position": { "x": 150, "y": 300 } } ``` **User Joined**: ```json { "event": "join", "user_id": "charlie@example.com", "timestamp": "2025-01-15T10:05:00Z" } ``` **User Left**: ```json { "event": "leave", "user_id": "bob@example.com", "timestamp": "2025-01-15T10:10:00Z" } ``` **Session Ended**: ```json { "event": "session_ended", "user_id": "alice@example.com", "message": "Session ended: host has left", "timestamp": "2025-01-15T10:15:00Z" } ``` **State Correction** (conflict detected): ```json { "message_type": "state_correction", "update_vector": 45 } ``` **Authorization Denied**: ```json { "message_type": "authorization_denied", "original_operation_id": "uuid", "reason": "insufficient_permissions" } ``` ### Echo Prevention **CRITICAL**: Never send WebSocket messages when applying remote operations. ```javascript 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 ```typescript 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 { 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 { 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 { // 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 ```python 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 ```http 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: ```http GET /oauth2/callback?code=AUTH_CODE&state=STATE ``` Response: ```json { "access_token": "eyJhbGc...", "token_type": "Bearer", "expires_in": 86400 } ``` #### 3. Use Token Include in Authorization header for all API requests: ```http Authorization: Bearer eyJhbGc... ``` ### Token Management **Get User Info**: ```http GET /oauth2/userinfo Authorization: Bearer {token} ``` Response: ```json { "sub": "google:123456789", "email": "alice@example.com", "name": "Alice Smith", "idp": "google" } ``` **Logout**: ```http POST /oauth2/logout Authorization: Bearer {token} ``` ### Test Provider (Development Only) For development, use the test OAuth provider: ```bash # 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: ```json { "error": "validation_error", "message": "Invalid input data", "details": { "field": "name", "reason": "Field is required" } } ``` ### WebSocket Errors **Authorization Denied**: ```json { "message_type": "authorization_denied", "original_operation_id": "uuid", "reason": "insufficient_permissions" } ``` **State Correction**: ```json { "message_type": "state_correction", "update_vector": 45 } ``` Handle by re-fetching diagram via REST API: ```javascript 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 ```javascript 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 - [Testing](Testing.md) - Learn testing strategies - [Extending TMI](Extending-TMI.md) - Build addons and integrations - [Architecture and Design](Architecture-and-Design.md) - Understand the architecture