Files
breakpilot-compliance/breakpilot-compliance-sdk/packages/core/src/client.ts
Benjamin Boenisch 4435e7ea0a Initial commit: breakpilot-compliance - Compliance SDK Platform
Services: Admin-Compliance, Backend-Compliance,
AI-Compliance-SDK, Consent-SDK, Developer-Portal,
PCA-Platform, DSMS

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:28 +01:00

522 lines
14 KiB
TypeScript

/**
* 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<string, AbortController> = 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<string, string> {
const headers: Record<string, string> = {
'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<Response> {
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<T>(
url: string,
options: RequestInit,
retries = this.maxRetries
): Promise<T> {
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<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}
// ---------------------------------------------------------------------------
// Authentication
// ---------------------------------------------------------------------------
async authenticate(request: AuthTokenRequest): Promise<AuthTokenResponse> {
const response = await this.fetchWithRetry<APIResponse<AuthTokenResponse>>(
`${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<AuthTokenResponse> {
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<StateResponse | null> {
try {
const response = await this.fetchWithRetry<APIResponse<StateResponse>>(
`${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<StateResponse> {
const response = await this.fetchWithRetry<APIResponse<StateResponse>>(
`${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<void> {
await this.fetchWithRetry<APIResponse<void>>(
`${this.apiEndpoint}/state?tenantId=${encodeURIComponent(this.tenantId)}`,
{
method: 'DELETE',
headers: this.getHeaders(),
}
)
}
// ---------------------------------------------------------------------------
// Checkpoints
// ---------------------------------------------------------------------------
async validateCheckpoint(
checkpointId: string,
data?: unknown
): Promise<CheckpointValidationResult> {
const response = await this.fetchWithRetry<APIResponse<CheckpointValidationResult>>(
`${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<Record<string, CheckpointStatus>> {
const response = await this.fetchWithRetry<APIResponse<Record<string, CheckpointStatus>>>(
`${this.apiEndpoint}/checkpoints?tenantId=${encodeURIComponent(this.tenantId)}`,
{
method: 'GET',
headers: this.getHeaders(),
}
)
return response.data || {}
}
// ---------------------------------------------------------------------------
// RAG
// ---------------------------------------------------------------------------
async searchRAG(request: RAGSearchRequest): Promise<RAGSearchResponse> {
const response = await this.fetchWithRetry<APIResponse<RAGSearchResponse>>(
`${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<RAGAskResponse> {
const response = await this.fetchWithRetry<APIResponse<RAGAskResponse>>(
`${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<Blob> {
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<string, unknown>
): 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<APIResponse<{ id: string; status: string }>>(
`${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<unknown> {
const response = await this.fetchWithRetry<APIResponse<unknown>>(
`${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<boolean> {
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
}