-
Notifications
You must be signed in to change notification settings - Fork 0
API Integration
This guide provides comprehensive information for integrating with TMI's REST API, WebSocket API, and building client applications.
- REST API Integration
- WebSocket API Integration
- Client Integration Patterns
- Authentication Integration
- Error Handling
- Best Practices
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)
# 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_modelsList 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}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.
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}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}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}| 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 |
TMI provides WebSocket-based real-time collaboration for simultaneous diagram editing.
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}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);
};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;
}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"
}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"
}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;
}
}
}A complete OAuth client implementation for web applications:
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:
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();
}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();
}
}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)GET /oauth2/authorize?idp=googleServer redirects to Google OAuth, then back to TMI with authorization code.
TMI processes the callback and returns tokens:
GET /oauth2/callback?code=AUTH_CODE&state=STATEResponse:
{
"access_token": "eyJhbGc...",
"token_type": "Bearer",
"expires_in": 86400
}Include in Authorization header for all API requests:
Authorization: Bearer eyJhbGc...Get User Info:
GET /oauth2/userinfo
Authorization: Bearer {token}Response:
{
"sub": "google:123456789",
"email": "alice@example.com",
"name": "Alice Smith",
"idp": "google"
}Logout:
POST /me/logout
Authorization: Bearer {token}Revoke Token (RFC 7009):
POST /oauth2/revoke
Content-Type: application/json
{
"token": "your_access_or_refresh_token"
}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:
- Client generates a random
code_verifier(43-128 characters) - Client computes
code_challenge = BASE64URL(SHA256(code_verifier)) - Client sends
code_challengeto authorization endpoint - Server stores
code_challengewith authorization code - Client exchanges code +
code_verifierfor tokens - Server validates:
SHA256(code_verifier) == stored_code_challenge
PKCE Helper Functions (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):
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):
-
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 supportplain)
Authorization Request with PKCE:
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:
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');For development, use the TMI OAuth provider:
# 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).
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?
-
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
-
Standards Compliance: Follows OAuth 2.0 implicit flow specification (RFC 6749)
-
Consistency: Both OAuth and SAML use the same token delivery method
Client-Side Token Extraction:
// 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:
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 <div>Authenticating...</div>;
}Important: Always read tokens from window.location.hash, not window.location.search.
All errors return JSON with consistent structure:
{
"error": "validation_error",
"message": "Invalid input data",
"details": {
"field": "name",
"reason": "Field is required"
}
}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);
}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}`);
}- 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
- 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
- Join via REST first: Always join collaboration session via REST API before WebSocket
-
Handle state sync: Process initial
diagram_state_syncmessage - 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
- 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
- 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
- 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
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
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)
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.
| 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 |
What Changed: POST and PUT operations now use Input schemas that exclude server-generated fields.
Affected Resources: Assets, Documents, Notes, Repositories
Migration:
- Remove
idfrom POST requests (server-generated) - Remove
metadatafrom POST requests (use metadata endpoints instead) - Remove
created_atandmodified_atfrom POST/PUT requests - Use
*Inputschemas for requests (AssetInput,DocumentInput,NoteInput,RepositoryInput)
Example:
// 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"
}What Changed: All resources now include created_at and modified_at timestamps in responses.
Migration: Update response type definitions to include timestamp fields:
interface Asset {
id: string;
name: string;
type: string;
description?: string;
metadata?: Metadata[];
created_at: string; // RFC3339 timestamp (NEW)
modified_at: string; // RFC3339 timestamp (NEW)
}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 |
What Changed: Note list endpoints now return summary schemas without the content field.
Migration: Fetch individual notes to get content:
// 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;All resources now support JSON Patch (RFC 6902) for partial updates:
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"}
]If using code generation tools:
- Download the new OpenAPI specification
- Regenerate client SDK using oapi-codegen, OpenAPI Generator, or Swagger Codegen
- Update code to use new
*Inputtypes for requests
# 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- TMI API Clients - Pre-built client libraries
- REST API Reference - Complete endpoint documentation
- API Specifications - OpenAPI and AsyncAPI specs
For TypeScript integration, here are the complete type definitions for collaboration sessions and WebSocket messages:
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';
}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;interface TMIClientConfig {
baseUrl: string;
jwtToken?: string;
}
interface TMICollaborativeClientConfig {
threatModelId: string;
diagramId: string;
jwtToken: string;
serverUrl?: string;
autoReconnect?: boolean;
maxReconnectAttempts?: number;
}- Testing - Learn testing strategies
- Extending TMI - Build addons and integrations
- Architecture and Design - Understand the architecture
- Using TMI for Threat Modeling
- Accessing TMI
- Creating Your First Threat Model
- Understanding the User Interface
- Working with Data Flow Diagrams
- Managing Threats
- Collaborative Threat Modeling
- Using Notes and Documentation
- Metadata and Extensions
- Planning Your Deployment
- Deploying TMI Server
- OCI Container Deployment
- Deploying TMI Web Application
- Setting Up Authentication
- Database Setup
- Component Integration
- Post-Deployment
- Monitoring and Health
- Database Operations
- Security Operations
- Performance and Scaling
- Maintenance Tasks