/** * Compliance Client * * Main entry point for the SDK. Handles API communication with * retry logic, timeout handling, and optimistic locking. */ import type { APIResponse, StateResponse, CheckpointValidationResult, AuthTokenRequest, AuthTokenResponse, RAGSearchRequest, RAGSearchResponse, RAGAskRequest, RAGAskResponse, ExportFormat, SDKState, CheckpointStatus, } from '@breakpilot/compliance-sdk-types' // ============================================================================= // TYPES // ============================================================================= export interface ComplianceClientOptions { apiEndpoint: string apiKey?: string tenantId: string timeout?: number maxRetries?: number onError?: (error: Error) => void onAuthError?: () => void } interface APIError extends Error { status?: number code?: string retryable: boolean } // ============================================================================= // CONSTANTS // ============================================================================= const DEFAULT_TIMEOUT = 30000 const DEFAULT_MAX_RETRIES = 3 const RETRY_DELAYS = [1000, 2000, 4000] // ============================================================================= // COMPLIANCE CLIENT // ============================================================================= export class ComplianceClient { private apiEndpoint: string private apiKey: string | null private tenantId: string private timeout: number private maxRetries: number private accessToken: string | null = null private abortControllers: Map = new Map() private onError?: (error: Error) => void private onAuthError?: () => void constructor(options: ComplianceClientOptions) { this.apiEndpoint = options.apiEndpoint.replace(/\/$/, '') this.apiKey = options.apiKey ?? null this.tenantId = options.tenantId this.timeout = options.timeout ?? DEFAULT_TIMEOUT this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES this.onError = options.onError this.onAuthError = options.onAuthError } // --------------------------------------------------------------------------- // Private Methods // --------------------------------------------------------------------------- private createError(message: string, status?: number, retryable = false): APIError { const error = new Error(message) as APIError error.status = status error.retryable = retryable return error } private getHeaders(): Record { const headers: Record = { 'Content-Type': 'application/json', 'X-Tenant-ID': this.tenantId, } if (this.accessToken) { headers['Authorization'] = `Bearer ${this.accessToken}` } else if (this.apiKey) { headers['Authorization'] = `Bearer ${this.apiKey}` } return headers } private async fetchWithTimeout( url: string, options: RequestInit, requestId: string ): Promise { const controller = new AbortController() this.abortControllers.set(requestId, controller) const timeoutId = setTimeout(() => controller.abort(), this.timeout) try { const response = await fetch(url, { ...options, signal: controller.signal, }) return response } finally { clearTimeout(timeoutId) this.abortControllers.delete(requestId) } } private async fetchWithRetry( url: string, options: RequestInit, retries = this.maxRetries ): Promise { const requestId = `${Date.now()}-${Math.random()}` let lastError: Error | null = null for (let attempt = 0; attempt <= retries; attempt++) { try { const response = await this.fetchWithTimeout(url, options, requestId) if (!response.ok) { const errorBody = await response.text() let errorMessage = `HTTP ${response.status}` try { const errorJson = JSON.parse(errorBody) errorMessage = errorJson.error || errorJson.message || errorMessage } catch { // Keep HTTP status message } // Handle auth errors if (response.status === 401) { this.onAuthError?.() throw this.createError(errorMessage, response.status, false) } // Don't retry client errors (4xx) except 429 const retryable = response.status >= 500 || response.status === 429 if (!retryable || attempt === retries) { throw this.createError(errorMessage, response.status, retryable) } } else { const data = await response.json() return data as T } } catch (error) { lastError = error as Error if (error instanceof Error && error.name === 'AbortError') { throw this.createError('Request timeout', 408, true) } const apiError = error as APIError if (!apiError.retryable || attempt === retries) { this.onError?.(error as Error) throw error } } // Exponential backoff if (attempt < retries) { await this.sleep(RETRY_DELAYS[attempt] || RETRY_DELAYS[RETRY_DELAYS.length - 1]) } } throw lastError || this.createError('Unknown error', 500, false) } private sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)) } // --------------------------------------------------------------------------- // Authentication // --------------------------------------------------------------------------- async authenticate(request: AuthTokenRequest): Promise { const response = await this.fetchWithRetry>( `${this.apiEndpoint}/auth/token`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(request), } ) if (response.success && response.data) { this.accessToken = response.data.accessToken return response.data } throw this.createError(response.error || 'Authentication failed', 401, false) } async refreshToken(refreshToken: string): Promise { return this.authenticate({ grantType: 'refresh_token', clientId: '', refreshToken, }) } setAccessToken(token: string): void { this.accessToken = token } clearAccessToken(): void { this.accessToken = null } // --------------------------------------------------------------------------- // State Management // --------------------------------------------------------------------------- async getState(): Promise { try { const response = await this.fetchWithRetry>( `${this.apiEndpoint}/state?tenantId=${encodeURIComponent(this.tenantId)}`, { method: 'GET', headers: this.getHeaders(), } ) if (response.success && response.data) { return response.data } return null } catch (error) { const apiError = error as APIError if (apiError.status === 404) { return null } throw error } } async saveState(state: SDKState, version?: number): Promise { const response = await this.fetchWithRetry>( `${this.apiEndpoint}/state`, { method: 'POST', headers: { ...this.getHeaders(), ...(version !== undefined && { 'If-Match': String(version) }), }, body: JSON.stringify({ tenantId: this.tenantId, state, version, }), } ) if (!response.success) { throw this.createError(response.error || 'Failed to save state', 500, true) } return response.data! } async deleteState(): Promise { await this.fetchWithRetry>( `${this.apiEndpoint}/state?tenantId=${encodeURIComponent(this.tenantId)}`, { method: 'DELETE', headers: this.getHeaders(), } ) } // --------------------------------------------------------------------------- // Checkpoints // --------------------------------------------------------------------------- async validateCheckpoint( checkpointId: string, data?: unknown ): Promise { const response = await this.fetchWithRetry>( `${this.apiEndpoint}/checkpoints/validate`, { method: 'POST', headers: this.getHeaders(), body: JSON.stringify({ tenantId: this.tenantId, checkpointId, data, }), } ) if (!response.success || !response.data) { throw this.createError(response.error || 'Checkpoint validation failed', 500, true) } return response.data } async getCheckpoints(): Promise> { const response = await this.fetchWithRetry>>( `${this.apiEndpoint}/checkpoints?tenantId=${encodeURIComponent(this.tenantId)}`, { method: 'GET', headers: this.getHeaders(), } ) return response.data || {} } // --------------------------------------------------------------------------- // RAG // --------------------------------------------------------------------------- async searchRAG(request: RAGSearchRequest): Promise { const response = await this.fetchWithRetry>( `${this.apiEndpoint}/rag/search`, { method: 'POST', headers: this.getHeaders(), body: JSON.stringify(request), } ) if (!response.success || !response.data) { throw this.createError(response.error || 'RAG search failed', 500, true) } return response.data } async askRAG(request: RAGAskRequest): Promise { const response = await this.fetchWithRetry>( `${this.apiEndpoint}/rag/ask`, { method: 'POST', headers: this.getHeaders(), body: JSON.stringify(request), } ) if (!response.success || !response.data) { throw this.createError(response.error || 'RAG query failed', 500, true) } return response.data } // --------------------------------------------------------------------------- // Export // --------------------------------------------------------------------------- async exportState(format: ExportFormat): Promise { const response = await this.fetchWithTimeout( `${this.apiEndpoint}/export?tenantId=${encodeURIComponent(this.tenantId)}&format=${format}`, { method: 'GET', headers: { ...this.getHeaders(), Accept: format === 'json' ? 'application/json' : format === 'pdf' ? 'application/pdf' : 'application/octet-stream', }, }, `export-${Date.now()}` ) if (!response.ok) { throw this.createError(`Export failed: ${response.statusText}`, response.status, true) } return response.blob() } // --------------------------------------------------------------------------- // Document Generation // --------------------------------------------------------------------------- async generateDocument( type: 'dsfa' | 'tom' | 'vvt' | 'gutachten' | 'privacy_policy' | 'cookie_banner', options?: Record ): Promise<{ id: string; status: string; content?: string }> { const response = await this.fetchWithRetry< APIResponse<{ id: string; status: string; content?: string }> >( `${this.apiEndpoint}/generate/${type}`, { method: 'POST', headers: this.getHeaders(), body: JSON.stringify({ tenantId: this.tenantId, options, }), } ) if (!response.success || !response.data) { throw this.createError(response.error || 'Document generation failed', 500, true) } return response.data } // --------------------------------------------------------------------------- // Security Scan // --------------------------------------------------------------------------- async startSecurityScan(options?: { tools?: string[] targetPath?: string severityThreshold?: string generateSBOM?: boolean }): Promise<{ id: string; status: string }> { const response = await this.fetchWithRetry>( `${this.apiEndpoint}/security/scan`, { method: 'POST', headers: this.getHeaders(), body: JSON.stringify({ tenantId: this.tenantId, ...options, }), } ) if (!response.success || !response.data) { throw this.createError(response.error || 'Security scan failed', 500, true) } return response.data } async getSecurityScanResult(scanId: string): Promise { const response = await this.fetchWithRetry>( `${this.apiEndpoint}/security/scan/${scanId}`, { method: 'GET', headers: this.getHeaders(), } ) return response.data } // --------------------------------------------------------------------------- // Utility // --------------------------------------------------------------------------- cancelAllRequests(): void { this.abortControllers.forEach(controller => controller.abort()) this.abortControllers.clear() } setTenantId(tenantId: string): void { this.tenantId = tenantId } getTenantId(): string { return this.tenantId } async healthCheck(): Promise { try { const response = await this.fetchWithTimeout( `${this.apiEndpoint}/health`, { method: 'GET' }, `health-${Date.now()}` ) return response.ok } catch { return false } } } // ============================================================================= // FACTORY // ============================================================================= let clientInstance: ComplianceClient | null = null export function getComplianceClient(options?: ComplianceClientOptions): ComplianceClient { if (!clientInstance && !options) { throw new Error('ComplianceClient not initialized. Provide options on first call.') } if (!clientInstance && options) { clientInstance = new ComplianceClient(options) } return clientInstance! } export function resetComplianceClient(): void { if (clientInstance) { clientInstance.cancelAllRequests() } clientInstance = null }