# 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 ### JavaScript OAuth Client A complete OAuth client implementation for web applications: ```javascript class TMIOAuth { constructor(tmiServerUrl = "http://localhost:8080") { this.tmiServerUrl = tmiServerUrl; this.providerConfig = { google: { authUrl: "https://accounts.google.com/o/oauth2/v2/auth", clientId: "your-google-client-id", scopes: "openid profile email", }, github: { authUrl: "https://github.com/login/oauth/authorize", clientId: "your-github-client-id", scopes: "user:email", }, microsoft: { authUrl: "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize", clientId: "your-microsoft-client-id", scopes: "openid profile email User.Read", }, }; } // Start OAuth login flow login(provider) { const config = this.providerConfig[provider]; if (!config) throw new Error(`Unsupported provider: ${provider}`); const state = this.generateState(); localStorage.setItem("oauth_state", state); localStorage.setItem("oauth_provider", provider); const params = new URLSearchParams({ client_id: config.clientId, redirect_uri: `${window.location.origin}/oauth2/callback`, response_type: "code", scope: config.scopes, state: state, }); window.location.href = `${config.authUrl}?${params}`; } // Handle OAuth callback (call this in your /oauth2/callback page) async handleCallback() { const urlParams = new URLSearchParams(window.location.search); const code = urlParams.get("code"); const state = urlParams.get("state"); const error = urlParams.get("error"); if (error) throw new Error(`OAuth error: ${error}`); if (!code || !state) throw new Error("Missing authorization code or state"); // Verify state for CSRF protection const storedState = localStorage.getItem("oauth_state"); const provider = localStorage.getItem("oauth_provider"); if (state !== storedState) throw new Error("Invalid state parameter"); localStorage.removeItem("oauth_state"); localStorage.removeItem("oauth_provider"); // Exchange code with TMI server const response = await fetch(`${this.tmiServerUrl}/oauth2/token?idp=${provider}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ grant_type: "authorization_code", code, state, redirect_uri: `${window.location.origin}/oauth2/callback`, }), }); if (!response.ok) { const error = await response.json(); throw new Error(`OAuth exchange failed: ${error.error}`); } const tokens = await response.json(); localStorage.setItem("tmi_access_token", tokens.access_token); localStorage.setItem("tmi_refresh_token", tokens.refresh_token); localStorage.setItem("tmi_token_expires", Date.now() + tokens.expires_in * 1000); return tokens; } // Make authenticated API calls async apiCall(endpoint, options = {}) { let token = localStorage.getItem("tmi_access_token"); const expiresAt = localStorage.getItem("tmi_token_expires"); if (expiresAt && Date.now() > parseInt(expiresAt) - 60000) { await this.refreshToken(); token = localStorage.getItem("tmi_access_token"); } return fetch(`${this.tmiServerUrl}${endpoint}`, { ...options, headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", ...options.headers, }, }); } // Refresh access token async refreshToken() { const refreshToken = localStorage.getItem("tmi_refresh_token"); if (!refreshToken) throw new Error("No refresh token available"); const response = await fetch(`${this.tmiServerUrl}/oauth2/refresh`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refresh_token: refreshToken }), }); if (!response.ok) { this.logout(); throw new Error("Token refresh failed - please login again"); } const tokens = await response.json(); localStorage.setItem("tmi_access_token", tokens.access_token); localStorage.setItem("tmi_refresh_token", tokens.refresh_token); localStorage.setItem("tmi_token_expires", Date.now() + tokens.expires_in * 1000); return tokens; } // Logout user async logout() { try { await fetch(`${this.tmiServerUrl}/me/logout`, { method: "POST", headers: { Authorization: `Bearer ${localStorage.getItem("tmi_access_token")}`, }, }); } catch (error) { console.warn("Logout request failed:", error); } localStorage.removeItem("tmi_access_token"); localStorage.removeItem("tmi_refresh_token"); localStorage.removeItem("tmi_token_expires"); } generateState() { return btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(32)))); } isLoggedIn() { const token = localStorage.getItem("tmi_access_token"); const expiresAt = localStorage.getItem("tmi_token_expires"); return token && expiresAt && Date.now() < parseInt(expiresAt); } } ``` **Usage Example**: ```javascript const tmiAuth = new TMIOAuth("http://localhost:8080"); // Login with Google document.getElementById("google-login").onclick = () => tmiAuth.login("google"); // Handle callback (in your /oauth2/callback page) if (window.location.pathname === "/oauth2/callback") { tmiAuth.handleCallback() .then(() => window.location.href = "/dashboard") .catch(error => { console.error("Login failed:", error); window.location.href = "/login?error=" + encodeURIComponent(error.message); }); } // Make API calls async function loadThreatModels() { const response = await tmiAuth.apiCall("/threat_models"); return response.json(); } ``` ### 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 /me/logout Authorization: Bearer {token} ``` **Revoke Token** (RFC 7009): ```http POST /oauth2/revoke Content-Type: application/json { "token": "your_access_or_refresh_token" } ``` ### PKCE Implementation (RFC 7636) TMI requires **PKCE (Proof Key for Code Exchange)** for all OAuth flows. PKCE prevents authorization code interception attacks and is essential for public clients (SPAs, mobile apps, desktop apps). **How PKCE Works**: 1. Client generates a random `code_verifier` (43-128 characters) 2. Client computes `code_challenge = BASE64URL(SHA256(code_verifier))` 3. Client sends `code_challenge` to authorization endpoint 4. Server stores `code_challenge` with authorization code 5. Client exchanges code + `code_verifier` for tokens 6. Server validates: `SHA256(code_verifier) == stored_code_challenge` **PKCE Helper Functions (JavaScript)**: ```javascript class PKCEHelper { // Generate cryptographically secure random code verifier static generateCodeVerifier() { const array = new Uint8Array(32); // 32 bytes = 256 bits crypto.getRandomValues(array); return this.base64URLEncode(array); } // Compute S256 challenge from verifier static async generateCodeChallenge(verifier) { const encoder = new TextEncoder(); const data = encoder.encode(verifier); const digest = await crypto.subtle.digest("SHA-256", data); return this.base64URLEncode(new Uint8Array(digest)); } // Base64URL encoding (without padding) static base64URLEncode(buffer) { const base64 = btoa(String.fromCharCode(...buffer)); return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); } } ``` **PKCE Helper Functions (Python)**: ```python import secrets import hashlib import base64 class PKCEHelper: @staticmethod def generate_code_verifier(): """Generate 43-character base64url-encoded string (32 random bytes).""" verifier_bytes = secrets.token_bytes(32) return base64.urlsafe_b64encode(verifier_bytes).decode('utf-8').rstrip('=') @staticmethod def generate_code_challenge(verifier): """Generate S256 code challenge: base64url(SHA256(verifier)).""" digest = hashlib.sha256(verifier.encode('utf-8')).digest() return base64.urlsafe_b64encode(digest).decode('utf-8').rstrip('=') ``` **PKCE Parameter Requirements** (per [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636)): - **code_verifier**: 43-128 characters, `[A-Z]` / `[a-z]` / `[0-9]` / `-` / `.` / `_` / `~` - **code_challenge**: Base64URL-encoded SHA-256 hash (43 characters) - **code_challenge_method**: Must be `S256` (TMI does not support `plain`) **Authorization Request with PKCE**: ```javascript const codeVerifier = PKCEHelper.generateCodeVerifier(); const codeChallenge = await PKCEHelper.generateCodeChallenge(codeVerifier); // Store verifier for token exchange sessionStorage.setItem('pkce_verifier', codeVerifier); // Build authorization URL const authUrl = `http://localhost:8080/oauth2/authorize?idp=google` + `&state=${generateRandomState()}` + `&client_callback=${encodeURIComponent(callbackUrl)}` + `&code_challenge=${encodeURIComponent(codeChallenge)}` + `&code_challenge_method=S256`; window.location.href = authUrl; ``` **Token Exchange with PKCE Verifier**: ```javascript const codeVerifier = sessionStorage.getItem('pkce_verifier'); const response = await fetch(`http://localhost:8080/oauth2/token?idp=google`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ grant_type: 'authorization_code', code: authorizationCode, code_verifier: codeVerifier, redirect_uri: callbackUrl }) }); sessionStorage.removeItem('pkce_verifier'); ``` ### TMI Provider (Development Only) For development, use the TMI OAuth provider: ```bash # Random test user curl "http://localhost:8080/oauth2/authorize?idp=tmi" # Specific test user with login_hint curl "http://localhost:8080/oauth2/authorize?idp=tmi&login_hint=alice" ``` **Note**: The `login_hint` parameter must be 3-20 characters, alphanumeric plus hyphens (e.g., `alice`, `qa-user`). ### OAuth Token Delivery via URL Fragments Both OAuth and SAML authentication flows deliver JWT tokens via **URL fragments** (the part after `#`) rather than query parameters (the part after `?`). **Token Delivery Format**: ``` https://your-app.com/callback#access_token=eyJhbGc...&refresh_token=abc123&token_type=Bearer&expires_in=3600&state=xyz ``` **Why URL Fragments?** 1. **Security**: URL fragments are never sent to the server, preventing tokens from appearing in: - Server access logs - Reverse proxy logs - Browser history (on most browsers) - Referrer headers when navigating away 2. **Standards Compliance**: Follows OAuth 2.0 implicit flow specification ([RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749)) 3. **Consistency**: Both OAuth and SAML use the same token delivery method **Client-Side Token Extraction**: ```typescript // Extract tokens from URL fragment ngOnInit() { const hash = window.location.hash.substring(1); // Remove leading '#' const params = new URLSearchParams(hash); const accessToken = params.get('access_token'); const refreshToken = params.get('refresh_token'); const tokenType = params.get('token_type'); const expiresIn = params.get('expires_in'); const state = params.get('state'); // For CSRF validation if (accessToken) { // Store tokens securely this.authService.setTokens({ accessToken, refreshToken, tokenType, expiresIn: parseInt(expiresIn || '3600', 10) }); // Clear fragment from URL to prevent token exposure window.history.replaceState({}, document.title, window.location.pathname); // Redirect to intended page this.router.navigate(['/']); } } ``` **React Example**: ```typescript import { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { useAuth } from './hooks/useAuth'; export function OAuthCallback() { const navigate = useNavigate(); const { setTokens } = useAuth(); useEffect(() => { // Extract tokens from URL fragment const hash = window.location.hash.substring(1); const params = new URLSearchParams(hash); const accessToken = params.get('access_token'); const refreshToken = params.get('refresh_token'); if (accessToken && refreshToken) { // Store tokens setTokens({ accessToken, refreshToken, tokenType: params.get('token_type') || 'Bearer', expiresIn: parseInt(params.get('expires_in') || '3600', 10) }); // Clear fragment window.history.replaceState({}, document.title, window.location.pathname); // Redirect navigate('/'); } else { navigate('/login'); } }, [navigate, setTokens]); return
Authenticating...
; } ``` **Important**: Always read tokens from `window.location.hash`, not `window.location.search`. ## 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 the TMI repository for complete code examples: **Current Integration Guides** (`/docs/developer/integration/`): - `client-oauth-integration.md` - OAuth 2.0 client patterns with PKCE support - `client-websocket-integration-guide.md` - Comprehensive WebSocket collaborative editing **Migrated Documentation** (`/docs/migrated/developer/integration/`): - `README.md` - Integration patterns overview and quick start guide ## 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) ## API v1.0.0 Migration Guide If you're migrating from a previous API version (v0.x) to v1.0.0, this section covers the key breaking changes and migration steps. ### Breaking Changes Summary | Category | Impact | Action Required | |----------|--------|----------------| | Request Schemas | **HIGH** | Update POST/PUT request bodies | | Response Schemas | **HIGH** | Handle new timestamp fields | | Batch Endpoints | **MEDIUM** | Migrate to bulk endpoints | | List Responses | **MEDIUM** | Handle Note summaries | | Bulk Operations | **LOW** | Optional - use new capabilities | | PATCH Support | **LOW** | Optional - use new endpoints | ### 1. Request Schema Changes **What Changed:** POST and PUT operations now use `Input` schemas that exclude server-generated fields. **Affected Resources:** Assets, Documents, Notes, Repositories **Migration:** - Remove `id` from POST requests (server-generated) - Remove `metadata` from POST requests (use metadata endpoints instead) - Remove `created_at` and `modified_at` from POST/PUT requests - Use `*Input` schemas for requests (`AssetInput`, `DocumentInput`, `NoteInput`, `RepositoryInput`) **Example:** ```json // BEFORE (v0.x) - Don't do this POST /threat_models/{id}/assets { "id": "6ba7b810-9dad-11d1-beef-00c04fd430c8", "name": "Customer Database", "type": "software", "metadata": [] } // AFTER (v1.0.0) - Do this POST /threat_models/{id}/assets { "name": "Customer Database", "type": "software" } ``` ### 2. Response Schema Changes **What Changed:** All resources now include `created_at` and `modified_at` timestamps in responses. **Migration:** Update response type definitions to include timestamp fields: ```typescript interface Asset { id: string; name: string; type: string; description?: string; metadata?: Metadata[]; created_at: string; // RFC3339 timestamp (NEW) modified_at: string; // RFC3339 timestamp (NEW) } ``` ### 3. Batch to Bulk Endpoint Migration **What Changed:** Removed `/batch` endpoints for threats. Use `/bulk` endpoints instead. | Operation | Old (v0.x) | New (v1.0.0) | |-----------|----------|--------| | Bulk Create | `POST /threats/bulk` | `POST /threats/bulk` (unchanged) | | Bulk Upsert | `PUT /threats/bulk` | `PUT /threats/bulk` (unchanged) | | Bulk Partial Update | `PATCH /threats/batch/patch` | `PATCH /threats/bulk` | | Bulk Delete | `DELETE /threats/batch` | `DELETE /threats/bulk` | ### 4. List Response Changes **What Changed:** Note list endpoints now return summary schemas without the `content` field. **Migration:** Fetch individual notes to get content: ```javascript // List notes (summary only) const notes = await GET(`/threat_models/${tmId}/notes`); // Get full note with content const fullNote = await GET(`/threat_models/${tmId}/notes/${notes[0].id}`); const content = fullNote.content; ``` ### 5. New PATCH Support All resources now support JSON Patch (RFC 6902) for partial updates: ```http PATCH /threat_models/{id}/assets/{asset_id} Content-Type: application/json-patch+json [ {"op": "replace", "path": "/name", "value": "Updated Name"}, {"op": "add", "path": "/description", "value": "New description"} ] ``` ### Code Generation If using code generation tools: 1. Download the new OpenAPI specification 2. Regenerate client SDK using oapi-codegen, OpenAPI Generator, or Swagger Codegen 3. Update code to use new `*Input` types for requests ```bash # Example with oapi-codegen oapi-codegen -package tmiclient tmi-openapi-v1.json > tmiclient.go # Example with OpenAPI Generator openapi-generator generate -i tmi-openapi-v1.json -g typescript-fetch -o ./src/client ``` ### Additional Resources - [TMI API Clients](TMI-API-Clients) - Pre-built client libraries - [REST API Reference](REST-API-Reference) - Complete endpoint documentation - [API Specifications](API-Specifications) - OpenAPI and AsyncAPI specs ## TypeScript Type Definitions For TypeScript integration, here are the complete type definitions for collaboration sessions and WebSocket messages: ### Collaboration Session Types ```typescript interface CollaborationSession { session_id: string; host: string; threat_model_id: string; threat_model_name: string; diagram_id: string; diagram_name: string; participants: SessionParticipant[]; websocket_url: string; } interface SessionParticipant { user_id: string; joined_at: string; // ISO 8601 timestamp permissions: 'reader' | 'writer' | 'owner'; } ``` ### WebSocket Message Types ```typescript interface DiagramOperationMessage { message_type: 'diagram_operation'; user_id: string; operation_id: string; sequence_number?: number; operation: CellPatchOperation; } interface CellPatchOperation { type: 'patch'; cells: CellOperation[]; } interface CellOperation { id: string; operation: 'add' | 'update' | 'remove'; data?: Cell; } interface Cell { id: string; shape: 'actor' | 'process' | 'store' | 'security-boundary' | 'text-box'; x: number; y: number; width: number; height: number; label: string; [key: string]: any; } interface DiagramStateSyncMessage { message_type: 'diagram_state_sync'; diagram_id: string; update_vector: number | null; cells: Cell[]; } interface CurrentPresenterMessage { message_type: 'current_presenter'; current_presenter: string; } interface PresenterCursorMessage { message_type: 'presenter_cursor'; user_id: string; cursor_position: { x: number; y: number }; } interface AuthorizationDeniedMessage { message_type: 'authorization_denied'; original_operation_id: string; reason: string; } interface StateCorrectionMessage { message_type: 'state_correction'; update_vector: number; } type WebSocketMessage = | DiagramOperationMessage | DiagramStateSyncMessage | CurrentPresenterMessage | PresenterCursorMessage | AuthorizationDeniedMessage | StateCorrectionMessage; ``` ### Client Configuration Types ```typescript interface TMIClientConfig { baseUrl: string; jwtToken?: string; } interface TMICollaborativeClientConfig { threatModelId: string; diagramId: string; jwtToken: string; serverUrl?: string; autoReconnect?: boolean; maxReconnectAttempts?: number; } ``` ## 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