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>
This commit is contained in:
Benjamin Boenisch
2026-02-11 23:47:28 +01:00
commit 4435e7ea0a
734 changed files with 251369 additions and 0 deletions

View File

@@ -0,0 +1,295 @@
/**
* Authentication Provider
*
* Manages authentication state and token lifecycle
*/
import type { AuthTokenResponse } from '@breakpilot/compliance-sdk-types'
import { ComplianceClient } from './client'
// =============================================================================
// TYPES
// =============================================================================
export interface AuthProviderOptions {
client: ComplianceClient
clientId: string
clientSecret?: string
storage?: Storage
onAuthStateChange?: (state: AuthState) => void
}
export interface AuthState {
isAuthenticated: boolean
accessToken: string | null
refreshToken: string | null
expiresAt: Date | null
user?: {
id: string
email: string
name: string
role: string
}
}
// =============================================================================
// CONSTANTS
// =============================================================================
const STORAGE_KEY = 'breakpilot-sdk-auth'
const TOKEN_REFRESH_BUFFER = 5 * 60 * 1000 // 5 minutes before expiry
// =============================================================================
// AUTH PROVIDER
// =============================================================================
export class AuthProvider {
private client: ComplianceClient
private clientId: string
private clientSecret?: string
private storage: Storage | null
private state: AuthState
private refreshTimeout: ReturnType<typeof setTimeout> | null = null
private onAuthStateChange?: (state: AuthState) => void
constructor(options: AuthProviderOptions) {
this.client = options.client
this.clientId = options.clientId
this.clientSecret = options.clientSecret
this.storage = options.storage ?? (typeof localStorage !== 'undefined' ? localStorage : null)
this.onAuthStateChange = options.onAuthStateChange
this.state = {
isAuthenticated: false,
accessToken: null,
refreshToken: null,
expiresAt: null,
}
this.loadFromStorage()
}
// ---------------------------------------------------------------------------
// Private Methods
// ---------------------------------------------------------------------------
private loadFromStorage(): void {
if (!this.storage) return
try {
const stored = this.storage.getItem(STORAGE_KEY)
if (stored) {
const data = JSON.parse(stored)
if (data.expiresAt) {
data.expiresAt = new Date(data.expiresAt)
}
// Check if token is still valid
if (data.expiresAt && data.expiresAt > new Date()) {
this.state = data
this.client.setAccessToken(data.accessToken)
this.scheduleTokenRefresh()
this.notifyStateChange()
} else if (data.refreshToken) {
// Try to refresh
this.refreshToken().catch(() => {
this.clearAuth()
})
}
}
} catch (error) {
console.error('Failed to load auth from storage:', error)
}
}
private saveToStorage(): void {
if (!this.storage) return
try {
this.storage.setItem(STORAGE_KEY, JSON.stringify(this.state))
} catch (error) {
console.error('Failed to save auth to storage:', error)
}
}
private clearStorage(): void {
if (!this.storage) return
try {
this.storage.removeItem(STORAGE_KEY)
} catch (error) {
console.error('Failed to clear auth from storage:', error)
}
}
private updateState(tokenResponse: AuthTokenResponse): void {
const expiresAt = new Date(Date.now() + tokenResponse.expiresIn * 1000)
this.state = {
isAuthenticated: true,
accessToken: tokenResponse.accessToken,
refreshToken: tokenResponse.refreshToken ?? this.state.refreshToken,
expiresAt,
}
this.client.setAccessToken(tokenResponse.accessToken)
this.saveToStorage()
this.scheduleTokenRefresh()
this.notifyStateChange()
}
private scheduleTokenRefresh(): void {
if (this.refreshTimeout) {
clearTimeout(this.refreshTimeout)
}
if (!this.state.expiresAt || !this.state.refreshToken) return
const timeUntilRefresh = this.state.expiresAt.getTime() - Date.now() - TOKEN_REFRESH_BUFFER
if (timeUntilRefresh > 0) {
this.refreshTimeout = setTimeout(() => {
this.refreshToken().catch(() => {
this.clearAuth()
})
}, timeUntilRefresh)
}
}
private notifyStateChange(): void {
this.onAuthStateChange?.(this.state)
}
// ---------------------------------------------------------------------------
// Public Methods
// ---------------------------------------------------------------------------
/**
* Authenticate using client credentials (server-to-server)
*/
async authenticateWithCredentials(): Promise<void> {
if (!this.clientSecret) {
throw new Error('Client secret is required for credentials authentication')
}
const response = await this.client.authenticate({
grantType: 'client_credentials',
clientId: this.clientId,
clientSecret: this.clientSecret,
})
this.updateState(response)
}
/**
* Authenticate using authorization code (OAuth flow)
*/
async authenticateWithCode(code: string, redirectUri: string): Promise<void> {
const response = await this.client.authenticate({
grantType: 'authorization_code',
clientId: this.clientId,
clientSecret: this.clientSecret,
code,
redirectUri,
})
this.updateState(response)
}
/**
* Refresh the access token
*/
async refreshToken(): Promise<void> {
if (!this.state.refreshToken) {
throw new Error('No refresh token available')
}
const response = await this.client.refreshToken(this.state.refreshToken)
this.updateState(response)
}
/**
* Set access token manually (e.g., from API key)
*/
setAccessToken(token: string, expiresIn?: number): void {
const expiresAt = expiresIn ? new Date(Date.now() + expiresIn * 1000) : null
this.state = {
isAuthenticated: true,
accessToken: token,
refreshToken: null,
expiresAt,
}
this.client.setAccessToken(token)
this.saveToStorage()
this.notifyStateChange()
}
/**
* Clear authentication state
*/
clearAuth(): void {
if (this.refreshTimeout) {
clearTimeout(this.refreshTimeout)
}
this.state = {
isAuthenticated: false,
accessToken: null,
refreshToken: null,
expiresAt: null,
}
this.client.clearAccessToken()
this.clearStorage()
this.notifyStateChange()
}
/**
* Get current authentication state
*/
getState(): AuthState {
return { ...this.state }
}
/**
* Check if currently authenticated
*/
isAuthenticated(): boolean {
if (!this.state.isAuthenticated || !this.state.accessToken) {
return false
}
if (this.state.expiresAt && this.state.expiresAt <= new Date()) {
return false
}
return true
}
/**
* Get the authorization URL for OAuth flow
*/
getAuthorizationUrl(redirectUri: string, scope?: string, state?: string): string {
const params = new URLSearchParams({
client_id: this.clientId,
redirect_uri: redirectUri,
response_type: 'code',
...(scope && { scope }),
...(state && { state }),
})
// This would be configured based on the API endpoint
return `/oauth/authorize?${params.toString()}`
}
/**
* Destroy the auth provider
*/
destroy(): void {
if (this.refreshTimeout) {
clearTimeout(this.refreshTimeout)
}
}
}

View File

@@ -0,0 +1,521 @@
/**
* 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
}

View File

@@ -0,0 +1,41 @@
/**
* @breakpilot/compliance-sdk-core
*
* Core functionality for BreakPilot Compliance SDK
*/
// Client
export { ComplianceClient, type ComplianceClientOptions } from './client'
// State Management
export {
createStore,
sdkReducer,
initialState,
type SDKStore,
type SDKStoreOptions,
} from './state'
// Sync
export {
StateSyncManager,
createStateSyncManager,
type SyncOptions,
type SyncCallbacks,
} from './sync'
// Auth
export {
AuthProvider,
type AuthProviderOptions,
type AuthState,
} from './auth'
// Modules
export * from './modules/dsgvo'
export * from './modules/compliance'
export * from './modules/rag'
export * from './modules/security'
// Utils
export * from './utils'

View File

@@ -0,0 +1,246 @@
/**
* Compliance Module
*
* General compliance functionality including controls, evidence,
* AI Act, NIS2, and regulatory obligations
*/
import type {
Control,
Evidence,
Requirement,
Obligation,
AIActResult,
AIActRiskCategory,
RegulationCode,
ComplianceScore,
SDKState,
RiskSeverity,
} from '@breakpilot/compliance-sdk-types'
import { ComplianceClient } from '../client'
export class ComplianceModule {
private client: ComplianceClient
private getState: () => SDKState
constructor(client: ComplianceClient, getState: () => SDKState) {
this.client = client
this.getState = getState
}
// ---------------------------------------------------------------------------
// Controls
// ---------------------------------------------------------------------------
getControls(): Control[] {
return this.getState().controls
}
getControlById(id: string): Control | undefined {
return this.getState().controls.find(c => c.id === id)
}
getControlsByDomain(domain: string): Control[] {
return this.getState().controls.filter(c => c.domain === domain)
}
getControlsByStatus(status: string): Control[] {
return this.getState().controls.filter(c => c.implementationStatus === status)
}
getControlComplianceRate(): number {
const controls = this.getControls()
if (controls.length === 0) return 0
const implemented = controls.filter(c => c.implementationStatus === 'IMPLEMENTED').length
return Math.round((implemented / controls.length) * 100)
}
// ---------------------------------------------------------------------------
// Evidence
// ---------------------------------------------------------------------------
getEvidence(): Evidence[] {
return this.getState().evidence
}
getEvidenceById(id: string): Evidence | undefined {
return this.getState().evidence.find(e => e.id === id)
}
getEvidenceByControlId(controlId: string): Evidence[] {
return this.getState().evidence.filter(e => e.controlId === controlId)
}
getExpiringEvidence(days: number = 30): Evidence[] {
const cutoff = new Date()
cutoff.setDate(cutoff.getDate() + days)
return this.getState().evidence.filter(e => {
if (!e.validUntil) return false
const validUntil = new Date(e.validUntil)
return validUntil <= cutoff && e.status === 'ACTIVE'
})
}
// ---------------------------------------------------------------------------
// Requirements
// ---------------------------------------------------------------------------
getRequirements(): Requirement[] {
return this.getState().requirements
}
getRequirementsByRegulation(regulation: RegulationCode): Requirement[] {
return this.getState().requirements.filter(r => r.regulationCode === regulation)
}
getRequirementComplianceRate(regulation?: RegulationCode): number {
let requirements = this.getRequirements()
if (regulation) {
requirements = requirements.filter(r => r.regulationCode === regulation)
}
if (requirements.length === 0) return 0
const implemented = requirements.filter(
r => r.status === 'IMPLEMENTED' || r.status === 'VERIFIED'
).length
return Math.round((implemented / requirements.length) * 100)
}
// ---------------------------------------------------------------------------
// Obligations
// ---------------------------------------------------------------------------
getObligations(): Obligation[] {
return this.getState().obligations
}
getUpcomingObligations(days: number = 30): Obligation[] {
const cutoff = new Date()
cutoff.setDate(cutoff.getDate() + days)
return this.getState().obligations.filter(o => {
if (!o.deadline || o.status === 'COMPLETED') return false
const deadline = new Date(o.deadline)
return deadline <= cutoff
})
}
getOverdueObligations(): Obligation[] {
const now = new Date()
return this.getState().obligations.filter(o => {
if (!o.deadline || o.status === 'COMPLETED') return false
const deadline = new Date(o.deadline)
return deadline < now
})
}
// ---------------------------------------------------------------------------
// AI Act
// ---------------------------------------------------------------------------
getAIActClassification(): AIActResult | null {
return this.getState().aiActClassification
}
getAIActRiskCategory(): AIActRiskCategory | null {
return this.getState().aiActClassification?.riskCategory ?? null
}
isHighRiskAI(): boolean {
const category = this.getAIActRiskCategory()
return category === 'HIGH' || category === 'UNACCEPTABLE'
}
// ---------------------------------------------------------------------------
// Compliance Score
// ---------------------------------------------------------------------------
calculateComplianceScore(): ComplianceScore {
const state = this.getState()
// Calculate overall score based on controls, requirements, and evidence
const controlScore = this.getControlComplianceRate()
const requirementScore = this.getRequirementComplianceRate()
const evidenceCoverage = this.calculateEvidenceCoverage()
const overall = Math.round((controlScore + requirementScore + evidenceCoverage) / 3)
// Calculate scores by regulation
const byRegulation: Record<string, number> = {}
const regulations = new Set(state.requirements.map(r => r.regulationCode))
regulations.forEach(reg => {
byRegulation[reg] = this.getRequirementComplianceRate(reg as RegulationCode)
})
// Calculate scores by domain
const byDomain: Record<string, number> = {}
const domains = new Set(state.controls.map(c => c.domain))
domains.forEach(domain => {
const domainControls = state.controls.filter(c => c.domain === domain)
const implemented = domainControls.filter(c => c.implementationStatus === 'IMPLEMENTED').length
byDomain[domain] = domainControls.length > 0
? Math.round((implemented / domainControls.length) * 100)
: 0
})
return {
overall,
byRegulation: byRegulation as Record<RegulationCode, number>,
byDomain: byDomain as Record<string, number>,
trend: 'STABLE', // Would need historical data to calculate
lastCalculated: new Date(),
}
}
private calculateEvidenceCoverage(): number {
const controls = this.getControls()
const implementedControls = controls.filter(c => c.implementationStatus === 'IMPLEMENTED')
if (implementedControls.length === 0) return 0
const controlsWithEvidence = implementedControls.filter(c => {
const evidence = this.getEvidenceByControlId(c.id)
return evidence.some(e => e.status === 'ACTIVE')
})
return Math.round((controlsWithEvidence.length / implementedControls.length) * 100)
}
// ---------------------------------------------------------------------------
// Risks
// ---------------------------------------------------------------------------
getRisks() {
return this.getState().risks
}
getRisksByStatus(status: string) {
return this.getRisks().filter(r => r.status === status)
}
getRisksBySeverity(severity: RiskSeverity) {
return this.getRisks().filter(r => r.severity === severity)
}
getCriticalRisks() {
return this.getRisks().filter(r => r.severity === 'CRITICAL' || r.severity === 'HIGH')
}
getAverageRiskScore(): number {
const risks = this.getRisks()
if (risks.length === 0) return 0
const totalScore = risks.reduce((sum, r) => sum + r.residualRiskScore, 0)
return Math.round(totalScore / risks.length)
}
}
export function createComplianceModule(
client: ComplianceClient,
getState: () => SDKState
): ComplianceModule {
return new ComplianceModule(client, getState)
}

View File

@@ -0,0 +1,155 @@
/**
* DSGVO Module
*
* GDPR compliance functionality
*/
import type {
DSRRequest,
DSRRequestType,
ConsentRecord,
ConsentPurpose,
ProcessingActivity,
DSFA,
TOM,
RetentionPolicy,
CookieBannerConfig,
SDKState,
} from '@breakpilot/compliance-sdk-types'
import { ComplianceClient } from '../client'
export class DSGVOModule {
private client: ComplianceClient
private getState: () => SDKState
constructor(client: ComplianceClient, getState: () => SDKState) {
this.client = client
this.getState = getState
}
// ---------------------------------------------------------------------------
// DSR (Data Subject Requests)
// ---------------------------------------------------------------------------
async submitDSR(type: DSRRequestType, requesterEmail: string, requesterName: string): Promise<DSRRequest> {
const response = await fetch(`${this.client.getTenantId()}/dsgvo/dsr`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type, requesterEmail, requesterName }),
})
return response.json()
}
getDSRRequests(): DSRRequest[] {
return this.getState().dsrRequests
}
getDSRById(id: string): DSRRequest | undefined {
return this.getState().dsrRequests.find(r => r.id === id)
}
// ---------------------------------------------------------------------------
// Consent Management
// ---------------------------------------------------------------------------
getConsents(): ConsentRecord[] {
return this.getState().consents
}
getConsentsByUserId(userId: string): ConsentRecord[] {
return this.getState().consents.filter(c => c.userId === userId)
}
hasConsent(userId: string, purpose: ConsentPurpose): boolean {
const consents = this.getConsentsByUserId(userId)
const latestConsent = consents
.filter(c => c.consentType === purpose)
.sort((a, b) => new Date(b.grantedAt).getTime() - new Date(a.grantedAt).getTime())[0]
return latestConsent?.granted && !latestConsent.revokedAt
}
// ---------------------------------------------------------------------------
// VVT (Processing Register)
// ---------------------------------------------------------------------------
getProcessingActivities(): ProcessingActivity[] {
return this.getState().vvt
}
getProcessingActivityById(id: string): ProcessingActivity | undefined {
return this.getState().vvt.find(p => p.id === id)
}
// ---------------------------------------------------------------------------
// DSFA
// ---------------------------------------------------------------------------
getDSFA(): DSFA | null {
return this.getState().dsfa
}
isDSFARequired(): boolean {
const state = this.getState()
const activeUseCase = state.useCases.find(uc => uc.id === state.activeUseCase)
return activeUseCase?.assessmentResult?.dsfaRequired ?? false
}
// ---------------------------------------------------------------------------
// TOMs
// ---------------------------------------------------------------------------
getTOMs(): TOM[] {
return this.getState().toms
}
getTOMsByCategory(category: string): TOM[] {
return this.getState().toms.filter(t => t.category === category)
}
getTOMScore(): number {
const toms = this.getTOMs()
if (toms.length === 0) return 0
const implemented = toms.filter(t => t.implementationStatus === 'IMPLEMENTED').length
return Math.round((implemented / toms.length) * 100)
}
// ---------------------------------------------------------------------------
// Retention Policies
// ---------------------------------------------------------------------------
getRetentionPolicies(): RetentionPolicy[] {
return this.getState().retentionPolicies
}
getUpcomingDeletions(days: number = 30): RetentionPolicy[] {
const cutoff = new Date()
cutoff.setDate(cutoff.getDate() + days)
return this.getState().retentionPolicies.filter(p => {
const nextReview = new Date(p.nextReviewDate)
return nextReview <= cutoff
})
}
// ---------------------------------------------------------------------------
// Cookie Banner
// ---------------------------------------------------------------------------
getCookieBannerConfig(): CookieBannerConfig | null {
return this.getState().cookieBanner
}
async generateCookieBannerCode(): Promise<{ html: string; css: string; js: string } | null> {
const config = this.getCookieBannerConfig()
if (!config) return null
const response = await this.client.generateDocument('cookie_banner', { config })
return response.content ? JSON.parse(response.content) : null
}
}
export function createDSGVOModule(client: ComplianceClient, getState: () => SDKState): DSGVOModule {
return new DSGVOModule(client, getState)
}

View File

@@ -0,0 +1,206 @@
/**
* RAG Module
*
* Legal RAG system for semantic search and AI-powered legal assistance
*/
import type {
SearchQuery,
SearchResponse,
AssistantQuery,
AssistantResponse,
LegalDocument,
ChatSession,
ChatMessage,
} from '@breakpilot/compliance-sdk-types'
import { ComplianceClient } from '../client'
export class RAGModule {
private client: ComplianceClient
private chatHistory: ChatMessage[] = []
private sessionId: string | null = null
constructor(client: ComplianceClient) {
this.client = client
}
// ---------------------------------------------------------------------------
// Search
// ---------------------------------------------------------------------------
async search(query: string, options?: Partial<SearchQuery>): Promise<SearchResponse> {
const searchRequest: SearchQuery = {
query,
limit: options?.limit ?? 10,
offset: options?.offset ?? 0,
filters: options?.filters,
includeMetadata: options?.includeMetadata ?? true,
scoreThreshold: options?.scoreThreshold ?? 0.5,
}
return this.client.searchRAG({
query: searchRequest.query,
filters: searchRequest.filters,
limit: searchRequest.limit,
offset: searchRequest.offset,
})
}
async searchByRegulation(regulation: string, query: string): Promise<SearchResponse> {
return this.search(query, {
filters: {
documentCodes: [regulation],
},
})
}
async searchByArticle(regulation: string, article: string): Promise<SearchResponse> {
return this.search(`${regulation} Artikel ${article}`, {
filters: {
documentCodes: [regulation],
articles: [article],
},
})
}
// ---------------------------------------------------------------------------
// Legal Assistant
// ---------------------------------------------------------------------------
async ask(question: string, options?: Partial<AssistantQuery>): Promise<AssistantResponse> {
const request: AssistantQuery = {
question,
context: options?.context,
documents: options?.documents,
maxSources: options?.maxSources ?? 5,
language: options?.language ?? 'de',
responseFormat: options?.responseFormat ?? 'detailed',
}
const response = await this.client.askRAG({
question: request.question,
context: request.context,
documents: request.documents,
maxSources: request.maxSources,
language: request.language,
})
// Add to chat history
this.chatHistory.push({
id: `user-${Date.now()}`,
role: 'user',
content: question,
timestamp: new Date(),
})
this.chatHistory.push({
id: `assistant-${Date.now()}`,
role: 'assistant',
content: response.answer,
timestamp: new Date(),
sources: response.sources,
})
return response
}
async askAboutRegulation(regulation: string, question: string): Promise<AssistantResponse> {
return this.ask(question, {
documents: [regulation],
context: `Kontext: Frage bezieht sich auf ${regulation}`,
})
}
async explainArticle(regulation: string, article: string): Promise<AssistantResponse> {
return this.ask(`Erkläre ${regulation} Artikel ${article} einfach und verständlich.`, {
documents: [regulation],
})
}
async checkCompliance(
regulation: string,
scenario: string
): Promise<AssistantResponse> {
return this.ask(
`Prüfe folgendes Szenario auf Compliance mit ${regulation}: ${scenario}`,
{
documents: [regulation],
responseFormat: 'detailed',
}
)
}
// ---------------------------------------------------------------------------
// Chat Session
// ---------------------------------------------------------------------------
startNewSession(): void {
this.sessionId = `session-${Date.now()}`
this.chatHistory = []
}
getChatHistory(): ChatMessage[] {
return [...this.chatHistory]
}
clearChatHistory(): void {
this.chatHistory = []
}
getSessionId(): string | null {
return this.sessionId
}
// ---------------------------------------------------------------------------
// Document Info
// ---------------------------------------------------------------------------
getAvailableRegulations(): readonly string[] {
return [
'DSGVO',
'AI_ACT',
'NIS2',
'EPRIVACY',
'TDDDG',
'SCC',
'DPF',
'CRA',
'EUCSA',
'DATA_ACT',
'DGA',
'DSA',
'EAA',
'BDSG',
'ISO_27001',
'BSI_GRUNDSCHUTZ',
'KRITIS',
'BAIT',
'VAIT',
'SOC2',
'PCI_DSS',
] as const
}
// ---------------------------------------------------------------------------
// Quick Actions
// ---------------------------------------------------------------------------
async getQuickAnswer(question: string): Promise<string> {
const response = await this.ask(question, {
responseFormat: 'concise',
maxSources: 3,
})
return response.answer
}
async findRelevantArticles(topic: string): Promise<SearchResponse> {
return this.search(topic, {
limit: 5,
scoreThreshold: 0.7,
})
}
}
export function createRAGModule(client: ComplianceClient): RAGModule {
return new RAGModule(client)
}

View File

@@ -0,0 +1,241 @@
/**
* Security Module
*
* Security scanning and SBOM management
*/
import type {
SBOM,
SecurityIssue,
SecurityScanResult,
BacklogItem,
SecurityIssueSeverity,
SecurityTool,
FindingsSummary,
SDKState,
} from '@breakpilot/compliance-sdk-types'
import { ComplianceClient } from '../client'
export class SecurityModule {
private client: ComplianceClient
private getState: () => SDKState
constructor(client: ComplianceClient, getState: () => SDKState) {
this.client = client
this.getState = getState
}
// ---------------------------------------------------------------------------
// Security Scanning
// ---------------------------------------------------------------------------
async startScan(options?: {
tools?: SecurityTool[]
targetPath?: string
severityThreshold?: SecurityIssueSeverity
generateSBOM?: boolean
}): Promise<{ id: string; status: string }> {
return this.client.startSecurityScan({
tools: options?.tools,
targetPath: options?.targetPath,
severityThreshold: options?.severityThreshold,
generateSBOM: options?.generateSBOM ?? true,
})
}
async getScanResult(scanId: string): Promise<SecurityScanResult | null> {
const result = await this.client.getSecurityScanResult(scanId)
return result as SecurityScanResult | null
}
getLastScanResult(): SecurityScanResult | null {
const screening = this.getState().screening
return screening?.securityScan ?? null
}
// ---------------------------------------------------------------------------
// SBOM
// ---------------------------------------------------------------------------
getSBOM(): SBOM | null {
return this.getState().sbom
}
getComponents() {
return this.getSBOM()?.components ?? []
}
getComponentsByLicense(license: string) {
return this.getComponents().filter(c => c.licenses.includes(license as never))
}
getVulnerableComponents() {
return this.getComponents().filter(c => c.vulnerabilities.length > 0)
}
getLicenseSummary(): Record<string, number> {
const components = this.getComponents()
const summary: Record<string, number> = {}
components.forEach(c => {
c.licenses.forEach(license => {
summary[license] = (summary[license] || 0) + 1
})
})
return summary
}
// ---------------------------------------------------------------------------
// Security Issues
// ---------------------------------------------------------------------------
getSecurityIssues(): SecurityIssue[] {
return this.getState().securityIssues
}
getIssueById(id: string): SecurityIssue | undefined {
return this.getSecurityIssues().find(i => i.id === id)
}
getIssuesBySeverity(severity: SecurityIssueSeverity): SecurityIssue[] {
return this.getSecurityIssues().filter(i => i.severity === severity)
}
getIssuesByStatus(status: string): SecurityIssue[] {
return this.getSecurityIssues().filter(i => i.status === status)
}
getIssuesByTool(tool: SecurityTool): SecurityIssue[] {
return this.getSecurityIssues().filter(i => i.tool === tool)
}
getOpenIssues(): SecurityIssue[] {
return this.getSecurityIssues().filter(i => i.status === 'OPEN' || i.status === 'IN_PROGRESS')
}
getCriticalIssues(): SecurityIssue[] {
return this.getSecurityIssues().filter(
i => (i.severity === 'CRITICAL' || i.severity === 'HIGH') && i.status === 'OPEN'
)
}
// ---------------------------------------------------------------------------
// Backlog
// ---------------------------------------------------------------------------
getBacklog(): BacklogItem[] {
return this.getState().securityBacklog
}
getBacklogByStatus(status: 'OPEN' | 'IN_PROGRESS' | 'DONE'): BacklogItem[] {
return this.getBacklog().filter(i => i.status === status)
}
getOverdueBacklogItems(): BacklogItem[] {
const now = new Date()
return this.getBacklog().filter(i => {
if (!i.dueDate || i.status === 'DONE') return false
return new Date(i.dueDate) < now
})
}
// ---------------------------------------------------------------------------
// Summary & Statistics
// ---------------------------------------------------------------------------
getSecuritySummary(): FindingsSummary {
const issues = this.getSecurityIssues()
const bySeverity: Record<SecurityIssueSeverity, number> = {
CRITICAL: 0,
HIGH: 0,
MEDIUM: 0,
LOW: 0,
INFO: 0,
}
const byStatus: Record<string, number> = {
OPEN: 0,
IN_PROGRESS: 0,
RESOLVED: 0,
ACCEPTED: 0,
FALSE_POSITIVE: 0,
}
const byTool: Record<string, number> = {}
issues.forEach(issue => {
bySeverity[issue.severity]++
byStatus[issue.status]++
byTool[issue.tool] = (byTool[issue.tool] || 0) + 1
})
// Calculate average resolution time for resolved issues
const resolvedIssues = issues.filter(i => i.status === 'RESOLVED' && i.resolvedAt)
let averageResolutionDays = 0
if (resolvedIssues.length > 0) {
const totalDays = resolvedIssues.reduce((sum, issue) => {
// Would need createdAt field to calculate properly
return sum + 7 // Placeholder
}, 0)
averageResolutionDays = Math.round(totalDays / resolvedIssues.length)
}
// Find oldest unresolved issue
const openIssues = issues.filter(i => i.status === 'OPEN')
let oldestUnresolvedDays = 0
// Would need createdAt field to calculate properly
return {
totalFindings: issues.length,
bySeverity,
byStatus: byStatus as Record<string, number>,
byTool: byTool as Record<SecurityTool, number>,
averageResolutionDays,
oldestUnresolvedDays,
}
}
getSecurityScore(): number {
const issues = this.getSecurityIssues()
const openIssues = issues.filter(i => i.status === 'OPEN' || i.status === 'IN_PROGRESS')
if (issues.length === 0) return 100
// Weight by severity
const severityWeights = {
CRITICAL: 10,
HIGH: 5,
MEDIUM: 2,
LOW: 1,
INFO: 0,
}
const totalWeight = issues.reduce((sum, i) => sum + severityWeights[i.severity], 0)
const openWeight = openIssues.reduce((sum, i) => sum + severityWeights[i.severity], 0)
if (totalWeight === 0) return 100
return Math.round(((totalWeight - openWeight) / totalWeight) * 100)
}
// ---------------------------------------------------------------------------
// Utilities
// ---------------------------------------------------------------------------
getAvailableTools(): SecurityTool[] {
return ['gitleaks', 'semgrep', 'bandit', 'trivy', 'grype', 'syft']
}
async exportSBOM(format: 'CycloneDX' | 'SPDX'): Promise<Blob> {
return this.client.exportState(format === 'CycloneDX' ? 'json' : 'json')
}
}
export function createSecurityModule(
client: ComplianceClient,
getState: () => SDKState
): SecurityModule {
return new SecurityModule(client, getState)
}

View File

@@ -0,0 +1,414 @@
/**
* SDK State Management
*
* Reducer-based state management with persistence support
*/
import type {
SDKState,
SDKAction,
UserPreferences,
SDKPhase,
SubscriptionTier,
getStepById,
} from '@breakpilot/compliance-sdk-types'
// =============================================================================
// INITIAL STATE
// =============================================================================
const initialPreferences: UserPreferences = {
language: 'de',
theme: 'light',
compactMode: false,
showHints: true,
autoSave: true,
autoValidate: true,
}
export const initialState: SDKState = {
// Metadata
version: '1.0.0',
lastModified: new Date(),
// Tenant & User
tenantId: '',
userId: '',
subscription: 'PROFESSIONAL' as SubscriptionTier,
// Progress
currentPhase: 1 as SDKPhase,
currentStep: 'use-case-workshop',
completedSteps: [],
checkpoints: {},
// Phase 1 Data
useCases: [],
activeUseCase: null,
screening: null,
modules: [],
requirements: [],
controls: [],
evidence: [],
checklist: [],
risks: [],
// Phase 2 Data
aiActClassification: null,
obligations: [],
dsfa: null,
toms: [],
retentionPolicies: [],
vvt: [],
documents: [],
cookieBanner: null,
consents: [],
dsrConfig: null,
dsrRequests: [],
escalationWorkflows: [],
// Security
sbom: null,
securityIssues: [],
securityBacklog: [],
// UI State
commandBarHistory: [],
recentSearches: [],
preferences: initialPreferences,
}
// =============================================================================
// REDUCER
// =============================================================================
export function sdkReducer(state: SDKState, action: SDKAction): SDKState {
const updateState = (updates: Partial<SDKState>): SDKState => ({
...state,
...updates,
lastModified: new Date(),
})
switch (action.type) {
case 'SET_STATE':
return updateState(action.payload)
case 'SET_CURRENT_STEP': {
// Import dynamically to avoid circular dependencies
const { getStepById } = require('@breakpilot/compliance-sdk-types')
const step = getStepById(action.payload)
return updateState({
currentStep: action.payload,
currentPhase: step?.phase || state.currentPhase,
})
}
case 'COMPLETE_STEP':
if (state.completedSteps.includes(action.payload)) {
return state
}
return updateState({
completedSteps: [...state.completedSteps, action.payload],
})
case 'SET_CHECKPOINT_STATUS':
return updateState({
checkpoints: {
...state.checkpoints,
[action.payload.id]: action.payload.status,
},
})
// Use Cases
case 'ADD_USE_CASE':
return updateState({
useCases: [...state.useCases, action.payload],
})
case 'UPDATE_USE_CASE':
return updateState({
useCases: state.useCases.map(uc =>
uc.id === action.payload.id ? { ...uc, ...action.payload.data } : uc
),
})
case 'DELETE_USE_CASE':
return updateState({
useCases: state.useCases.filter(uc => uc.id !== action.payload),
activeUseCase: state.activeUseCase === action.payload ? null : state.activeUseCase,
})
case 'SET_ACTIVE_USE_CASE':
return updateState({ activeUseCase: action.payload })
// Screening
case 'SET_SCREENING':
return updateState({ screening: action.payload })
// Modules
case 'ADD_MODULE':
return updateState({
modules: [...state.modules, action.payload],
})
case 'UPDATE_MODULE':
return updateState({
modules: state.modules.map(m =>
m.id === action.payload.id ? { ...m, ...action.payload.data } : m
),
})
// Requirements
case 'ADD_REQUIREMENT':
return updateState({
requirements: [...state.requirements, action.payload],
})
case 'UPDATE_REQUIREMENT':
return updateState({
requirements: state.requirements.map(r =>
r.id === action.payload.id ? { ...r, ...action.payload.data } : r
),
})
// Controls
case 'ADD_CONTROL':
return updateState({
controls: [...state.controls, action.payload],
})
case 'UPDATE_CONTROL':
return updateState({
controls: state.controls.map(c =>
c.id === action.payload.id ? { ...c, ...action.payload.data } : c
),
})
// Evidence
case 'ADD_EVIDENCE':
return updateState({
evidence: [...state.evidence, action.payload],
})
case 'UPDATE_EVIDENCE':
return updateState({
evidence: state.evidence.map(e =>
e.id === action.payload.id ? { ...e, ...action.payload.data } : e
),
})
case 'DELETE_EVIDENCE':
return updateState({
evidence: state.evidence.filter(e => e.id !== action.payload),
})
// Risks
case 'ADD_RISK':
return updateState({
risks: [...state.risks, action.payload],
})
case 'UPDATE_RISK':
return updateState({
risks: state.risks.map(r =>
r.id === action.payload.id ? { ...r, ...action.payload.data } : r
),
})
case 'DELETE_RISK':
return updateState({
risks: state.risks.filter(r => r.id !== action.payload),
})
// AI Act
case 'SET_AI_ACT_RESULT':
return updateState({ aiActClassification: action.payload })
// Obligations
case 'ADD_OBLIGATION':
return updateState({
obligations: [...state.obligations, action.payload],
})
case 'UPDATE_OBLIGATION':
return updateState({
obligations: state.obligations.map(o =>
o.id === action.payload.id ? { ...o, ...action.payload.data } : o
),
})
// DSFA
case 'SET_DSFA':
return updateState({ dsfa: action.payload })
// TOMs
case 'ADD_TOM':
return updateState({
toms: [...state.toms, action.payload],
})
case 'UPDATE_TOM':
return updateState({
toms: state.toms.map(t =>
t.id === action.payload.id ? { ...t, ...action.payload.data } : t
),
})
// Retention Policies
case 'ADD_RETENTION_POLICY':
return updateState({
retentionPolicies: [...state.retentionPolicies, action.payload],
})
case 'UPDATE_RETENTION_POLICY':
return updateState({
retentionPolicies: state.retentionPolicies.map(p =>
p.id === action.payload.id ? { ...p, ...action.payload.data } : p
),
})
// Processing Activities (VVT)
case 'ADD_PROCESSING_ACTIVITY':
return updateState({
vvt: [...state.vvt, action.payload],
})
case 'UPDATE_PROCESSING_ACTIVITY':
return updateState({
vvt: state.vvt.map(p =>
p.id === action.payload.id ? { ...p, ...action.payload.data } : p
),
})
// Documents
case 'ADD_DOCUMENT':
return updateState({
documents: [...state.documents, action.payload],
})
case 'UPDATE_DOCUMENT':
return updateState({
documents: state.documents.map(d =>
d.id === action.payload.id ? { ...d, ...action.payload.data } : d
),
})
// Cookie Banner
case 'SET_COOKIE_BANNER':
return updateState({ cookieBanner: action.payload })
// DSR
case 'SET_DSR_CONFIG':
return updateState({ dsrConfig: action.payload })
case 'ADD_DSR_REQUEST':
return updateState({
dsrRequests: [...state.dsrRequests, action.payload],
})
case 'UPDATE_DSR_REQUEST':
return updateState({
dsrRequests: state.dsrRequests.map(r =>
r.id === action.payload.id ? { ...r, ...action.payload.data } : r
),
})
// Escalation Workflows
case 'ADD_ESCALATION_WORKFLOW':
return updateState({
escalationWorkflows: [...state.escalationWorkflows, action.payload],
})
case 'UPDATE_ESCALATION_WORKFLOW':
return updateState({
escalationWorkflows: state.escalationWorkflows.map(w =>
w.id === action.payload.id ? { ...w, ...action.payload.data } : w
),
})
// Security
case 'ADD_SECURITY_ISSUE':
return updateState({
securityIssues: [...state.securityIssues, action.payload],
})
case 'UPDATE_SECURITY_ISSUE':
return updateState({
securityIssues: state.securityIssues.map(i =>
i.id === action.payload.id ? { ...i, ...action.payload.data } : i
),
})
case 'ADD_BACKLOG_ITEM':
return updateState({
securityBacklog: [...state.securityBacklog, action.payload],
})
case 'UPDATE_BACKLOG_ITEM':
return updateState({
securityBacklog: state.securityBacklog.map(i =>
i.id === action.payload.id ? { ...i, ...action.payload.data } : i
),
})
// UI State
case 'ADD_COMMAND_HISTORY':
return updateState({
commandBarHistory: [action.payload, ...state.commandBarHistory].slice(0, 50),
})
case 'SET_PREFERENCES':
return updateState({
preferences: { ...state.preferences, ...action.payload },
})
case 'RESET_STATE':
return { ...initialState, lastModified: new Date() }
default:
return state
}
}
// =============================================================================
// STORE
// =============================================================================
export interface SDKStoreOptions {
tenantId: string
userId: string
initialState?: Partial<SDKState>
onChange?: (state: SDKState) => void
}
export interface SDKStore {
getState: () => SDKState
dispatch: (action: SDKAction) => void
subscribe: (listener: (state: SDKState) => void) => () => void
}
export function createStore(options: SDKStoreOptions): SDKStore {
let state: SDKState = {
...initialState,
tenantId: options.tenantId,
userId: options.userId,
...options.initialState,
}
const listeners = new Set<(state: SDKState) => void>()
const getState = () => state
const dispatch = (action: SDKAction) => {
state = sdkReducer(state, action)
listeners.forEach(listener => listener(state))
options.onChange?.(state)
}
const subscribe = (listener: (state: SDKState) => void) => {
listeners.add(listener)
return () => listeners.delete(listener)
}
return { getState, dispatch, subscribe }
}

View File

@@ -0,0 +1,435 @@
/**
* SDK State Synchronization
*
* Handles offline/online sync, multi-tab coordination,
* and conflict resolution for SDK state.
*/
import type { SDKState, SyncState, SyncStatus, ConflictResolution } from '@breakpilot/compliance-sdk-types'
import { ComplianceClient } from './client'
// =============================================================================
// TYPES
// =============================================================================
export interface SyncOptions {
debounceMs?: number
maxRetries?: number
conflictHandler?: (local: SDKState, server: SDKState) => Promise<ConflictResolution>
}
export interface SyncCallbacks {
onSyncStart?: () => void
onSyncComplete?: (state: SDKState) => void
onSyncError?: (error: Error) => void
onConflict?: (local: SDKState, server: SDKState) => void
onOffline?: () => void
onOnline?: () => void
}
// =============================================================================
// CONSTANTS
// =============================================================================
const STORAGE_KEY_PREFIX = 'breakpilot-compliance-sdk-state'
const SYNC_CHANNEL = 'breakpilot-sdk-state-sync'
const DEFAULT_DEBOUNCE_MS = 2000
const DEFAULT_MAX_RETRIES = 3
// =============================================================================
// STATE SYNC MANAGER
// =============================================================================
export class StateSyncManager {
private client: ComplianceClient
private tenantId: string
private options: Required<SyncOptions>
private callbacks: SyncCallbacks
private syncState: SyncState
private broadcastChannel: BroadcastChannel | null = null
private debounceTimeout: ReturnType<typeof setTimeout> | null = null
private pendingState: SDKState | null = null
private isOnline = true
constructor(
client: ComplianceClient,
tenantId: string,
options: SyncOptions = {},
callbacks: SyncCallbacks = {}
) {
this.client = client
this.tenantId = tenantId
this.callbacks = callbacks
this.options = {
debounceMs: options.debounceMs ?? DEFAULT_DEBOUNCE_MS,
maxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES,
conflictHandler: options.conflictHandler ?? this.defaultConflictHandler.bind(this),
}
this.syncState = {
status: 'idle' as SyncStatus,
lastSyncedAt: null,
localVersion: 0,
serverVersion: 0,
pendingChanges: 0,
error: null,
}
this.setupBroadcastChannel()
this.setupOnlineListener()
}
// ---------------------------------------------------------------------------
// Setup Methods
// ---------------------------------------------------------------------------
private setupBroadcastChannel(): void {
if (typeof window === 'undefined' || !('BroadcastChannel' in window)) {
return
}
try {
this.broadcastChannel = new BroadcastChannel(`${SYNC_CHANNEL}-${this.tenantId}`)
this.broadcastChannel.onmessage = this.handleBroadcastMessage.bind(this)
} catch (error) {
console.warn('BroadcastChannel not available:', error)
}
}
private setupOnlineListener(): void {
if (typeof window === 'undefined') {
return
}
window.addEventListener('online', () => {
this.isOnline = true
this.syncState.status = 'idle'
this.callbacks.onOnline?.()
if (this.pendingState) {
this.syncToServer(this.pendingState)
}
})
window.addEventListener('offline', () => {
this.isOnline = false
this.syncState.status = 'offline'
this.callbacks.onOffline?.()
})
this.isOnline = navigator.onLine
if (!this.isOnline) {
this.syncState.status = 'offline'
}
}
// ---------------------------------------------------------------------------
// Broadcast Channel Methods
// ---------------------------------------------------------------------------
private handleBroadcastMessage(event: MessageEvent): void {
const { type, state, version } = event.data
switch (type) {
case 'STATE_UPDATED':
if (version > this.syncState.localVersion) {
this.syncState.localVersion = version
this.saveToLocalStorage(state)
this.callbacks.onSyncComplete?.(state)
}
break
case 'SYNC_COMPLETE':
this.syncState.serverVersion = version
break
case 'REQUEST_STATE':
this.broadcastState()
break
}
}
private broadcastState(): void {
if (!this.broadcastChannel) return
const state = this.loadFromLocalStorage()
if (state) {
this.broadcastChannel.postMessage({
type: 'STATE_UPDATED',
state,
version: this.syncState.localVersion,
tabId: this.getTabId(),
})
}
}
private broadcastSyncComplete(version: number): void {
if (!this.broadcastChannel) return
this.broadcastChannel.postMessage({
type: 'SYNC_COMPLETE',
version,
tabId: this.getTabId(),
})
}
private getTabId(): string {
if (typeof window === 'undefined') return 'server'
let tabId = sessionStorage.getItem('breakpilot-sdk-tab-id')
if (!tabId) {
tabId = `tab-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
sessionStorage.setItem('breakpilot-sdk-tab-id', tabId)
}
return tabId
}
// ---------------------------------------------------------------------------
// Local Storage Methods
// ---------------------------------------------------------------------------
private getStorageKey(): string {
return `${STORAGE_KEY_PREFIX}-${this.tenantId}`
}
saveToLocalStorage(state: SDKState): void {
if (typeof window === 'undefined') return
try {
const data = {
state,
version: this.syncState.localVersion,
savedAt: new Date().toISOString(),
}
localStorage.setItem(this.getStorageKey(), JSON.stringify(data))
} catch (error) {
console.error('Failed to save to localStorage:', error)
}
}
loadFromLocalStorage(): SDKState | null {
if (typeof window === 'undefined') return null
try {
const stored = localStorage.getItem(this.getStorageKey())
if (stored) {
const data = JSON.parse(stored)
this.syncState.localVersion = data.version || 0
return data.state
}
} catch (error) {
console.error('Failed to load from localStorage:', error)
}
return null
}
clearLocalStorage(): void {
if (typeof window === 'undefined') return
try {
localStorage.removeItem(this.getStorageKey())
} catch (error) {
console.error('Failed to clear localStorage:', error)
}
}
// ---------------------------------------------------------------------------
// Sync Methods
// ---------------------------------------------------------------------------
queueSync(state: SDKState): void {
this.pendingState = state
this.syncState.pendingChanges++
this.syncState.localVersion++
this.saveToLocalStorage(state)
this.broadcastState()
if (this.debounceTimeout) {
clearTimeout(this.debounceTimeout)
}
this.debounceTimeout = setTimeout(() => {
this.syncToServer(state)
}, this.options.debounceMs)
}
async forceSync(state: SDKState): Promise<void> {
if (this.debounceTimeout) {
clearTimeout(this.debounceTimeout)
this.debounceTimeout = null
}
await this.syncToServer(state)
}
private async syncToServer(state: SDKState): Promise<void> {
if (!this.isOnline) {
this.syncState.status = 'offline'
return
}
this.syncState.status = 'syncing'
this.callbacks.onSyncStart?.()
try {
const response = await this.client.saveState(state, this.syncState.serverVersion)
this.syncState = {
...this.syncState,
status: 'idle',
lastSyncedAt: new Date(),
serverVersion: response.version,
pendingChanges: 0,
error: null,
}
this.pendingState = null
this.broadcastSyncComplete(response.version)
this.callbacks.onSyncComplete?.(state)
} catch (error) {
if ((error as { status?: number }).status === 409) {
await this.handleConflict(state)
} else {
this.syncState.status = 'error'
this.syncState.error = (error as Error).message
this.callbacks.onSyncError?.(error as Error)
}
}
}
async loadFromServer(): Promise<SDKState | null> {
if (!this.isOnline) {
return this.loadFromLocalStorage()
}
try {
const response = await this.client.getState()
if (response) {
this.syncState.serverVersion = response.version
this.syncState.localVersion = response.version
this.saveToLocalStorage(response.state)
return response.state
}
return this.loadFromLocalStorage()
} catch (error) {
console.error('Failed to load from server:', error)
return this.loadFromLocalStorage()
}
}
// ---------------------------------------------------------------------------
// Conflict Resolution
// ---------------------------------------------------------------------------
private async handleConflict(localState: SDKState): Promise<void> {
this.syncState.status = 'conflict'
try {
const serverResponse = await this.client.getState()
if (!serverResponse) {
await this.client.saveState(localState)
return
}
const serverState = serverResponse.state
this.callbacks.onConflict?.(localState, serverState)
const resolution = await this.options.conflictHandler(localState, serverState)
let resolvedState: SDKState
switch (resolution.strategy) {
case 'local':
resolvedState = localState
break
case 'server':
resolvedState = serverState
break
case 'merge':
resolvedState = resolution.mergedState || localState
break
}
const response = await this.client.saveState(resolvedState)
this.syncState.serverVersion = response.version
this.syncState.localVersion = response.version
this.saveToLocalStorage(resolvedState)
this.syncState.status = 'idle'
this.callbacks.onSyncComplete?.(resolvedState)
} catch (error) {
this.syncState.status = 'error'
this.syncState.error = (error as Error).message
this.callbacks.onSyncError?.(error as Error)
}
}
private async defaultConflictHandler(
local: SDKState,
server: SDKState
): Promise<ConflictResolution> {
const localTime = new Date(local.lastModified).getTime()
const serverTime = new Date(server.lastModified).getTime()
if (localTime > serverTime) {
return { strategy: 'local' }
}
const mergedState: SDKState = {
...server,
preferences: local.preferences,
commandBarHistory: [
...local.commandBarHistory,
...server.commandBarHistory.filter(
h => !local.commandBarHistory.some(lh => lh.id === h.id)
),
].slice(0, 50),
recentSearches: [...new Set([...local.recentSearches, ...server.recentSearches])].slice(
0,
20
),
}
return { strategy: 'merge', mergedState }
}
// ---------------------------------------------------------------------------
// Getters & Cleanup
// ---------------------------------------------------------------------------
getSyncState(): SyncState {
return { ...this.syncState }
}
isOnlineStatus(): boolean {
return this.isOnline
}
hasPendingChanges(): boolean {
return this.syncState.pendingChanges > 0 || this.pendingState !== null
}
destroy(): void {
if (this.debounceTimeout) {
clearTimeout(this.debounceTimeout)
}
if (this.broadcastChannel) {
this.broadcastChannel.close()
}
}
}
// =============================================================================
// FACTORY
// =============================================================================
export function createStateSyncManager(
client: ComplianceClient,
tenantId: string,
options?: SyncOptions,
callbacks?: SyncCallbacks
): StateSyncManager {
return new StateSyncManager(client, tenantId, options, callbacks)
}

View File

@@ -0,0 +1,262 @@
/**
* Utility Functions
*/
// =============================================================================
// ID GENERATION
// =============================================================================
export function generateId(prefix?: string): string {
const timestamp = Date.now().toString(36)
const random = Math.random().toString(36).substring(2, 9)
return prefix ? `${prefix}-${timestamp}-${random}` : `${timestamp}-${random}`
}
export function generateUUID(): string {
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return crypto.randomUUID()
}
// Fallback for older environments
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = (Math.random() * 16) | 0
const v = c === 'x' ? r : (r & 0x3) | 0x8
return v.toString(16)
})
}
// =============================================================================
// DATE UTILITIES
// =============================================================================
export function formatDate(date: Date | string, locale = 'de-DE'): string {
const d = typeof date === 'string' ? new Date(date) : date
return d.toLocaleDateString(locale, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
})
}
export function formatDateTime(date: Date | string, locale = 'de-DE'): string {
const d = typeof date === 'string' ? new Date(date) : date
return d.toLocaleString(locale, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
})
}
export function isDateExpired(date: Date | string): boolean {
const d = typeof date === 'string' ? new Date(date) : date
return d < new Date()
}
export function addDays(date: Date, days: number): Date {
const result = new Date(date)
result.setDate(result.getDate() + days)
return result
}
export function daysBetween(date1: Date, date2: Date): number {
const oneDay = 24 * 60 * 60 * 1000
return Math.round(Math.abs((date1.getTime() - date2.getTime()) / oneDay))
}
// =============================================================================
// STRING UTILITIES
// =============================================================================
export function truncate(str: string, maxLength: number, suffix = '...'): string {
if (str.length <= maxLength) return str
return str.substring(0, maxLength - suffix.length) + suffix
}
export function slugify(str: string): string {
return str
.toLowerCase()
.replace(/[äöü]/g, c => ({ ä: 'ae', ö: 'oe', ü: 'ue' })[c] || c)
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '')
}
export function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase()
}
// =============================================================================
// ARRAY UTILITIES
// =============================================================================
export function groupBy<T, K extends string | number>(
array: T[],
keyFn: (item: T) => K
): Record<K, T[]> {
return array.reduce(
(groups, item) => {
const key = keyFn(item)
if (!groups[key]) {
groups[key] = []
}
groups[key].push(item)
return groups
},
{} as Record<K, T[]>
)
}
export function uniqueBy<T>(array: T[], keyFn: (item: T) => unknown): T[] {
const seen = new Set()
return array.filter(item => {
const key = keyFn(item)
if (seen.has(key)) return false
seen.add(key)
return true
})
}
export function sortBy<T>(array: T[], keyFn: (item: T) => number | string, desc = false): T[] {
return [...array].sort((a, b) => {
const aKey = keyFn(a)
const bKey = keyFn(b)
if (aKey < bKey) return desc ? 1 : -1
if (aKey > bKey) return desc ? -1 : 1
return 0
})
}
// =============================================================================
// OBJECT UTILITIES
// =============================================================================
export function deepClone<T>(obj: T): T {
if (obj === null || typeof obj !== 'object') return obj
if (obj instanceof Date) return new Date(obj.getTime()) as T
if (Array.isArray(obj)) return obj.map(item => deepClone(item)) as T
return Object.fromEntries(
Object.entries(obj as object).map(([key, value]) => [key, deepClone(value)])
) as T
}
export function deepMerge<T extends Record<string, unknown>>(target: T, source: Partial<T>): T {
const result = { ...target }
for (const key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
const sourceValue = source[key]
const targetValue = target[key]
if (
typeof sourceValue === 'object' &&
sourceValue !== null &&
!Array.isArray(sourceValue) &&
typeof targetValue === 'object' &&
targetValue !== null &&
!Array.isArray(targetValue)
) {
result[key] = deepMerge(
targetValue as Record<string, unknown>,
sourceValue as Record<string, unknown>
) as T[Extract<keyof T, string>]
} else {
result[key] = sourceValue as T[Extract<keyof T, string>]
}
}
}
return result
}
export function pick<T extends object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
return keys.reduce(
(result, key) => {
if (key in obj) {
result[key] = obj[key]
}
return result
},
{} as Pick<T, K>
)
}
export function omit<T extends object, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> {
const result = { ...obj }
keys.forEach(key => delete result[key])
return result
}
// =============================================================================
// VALIDATION UTILITIES
// =============================================================================
export function isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(email)
}
export function isValidUrl(url: string): boolean {
try {
new URL(url)
return true
} catch {
return false
}
}
export function isEmpty(value: unknown): boolean {
if (value === null || value === undefined) return true
if (typeof value === 'string') return value.trim().length === 0
if (Array.isArray(value)) return value.length === 0
if (typeof value === 'object') return Object.keys(value).length === 0
return false
}
// =============================================================================
// ASYNC UTILITIES
// =============================================================================
export function debounce<T extends (...args: unknown[]) => unknown>(
fn: T,
delay: number
): (...args: Parameters<T>) => void {
let timeoutId: ReturnType<typeof setTimeout>
return (...args: Parameters<T>) => {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => fn(...args), delay)
}
}
export function throttle<T extends (...args: unknown[]) => unknown>(
fn: T,
limit: number
): (...args: Parameters<T>) => void {
let lastCall = 0
return (...args: Parameters<T>) => {
const now = Date.now()
if (now - lastCall >= limit) {
lastCall = now
fn(...args)
}
}
}
export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}
export async function retry<T>(
fn: () => Promise<T>,
maxRetries: number,
delay: number
): Promise<T> {
let lastError: Error | null = null
for (let i = 0; i <= maxRetries; i++) {
try {
return await fn()
} catch (error) {
lastError = error as Error
if (i < maxRetries) {
await sleep(delay * Math.pow(2, i))
}
}
}
throw lastError
}