From 5cb91e88d2e6cbb830ffb3d14b19570d442388ad Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com> Date: Sat, 11 Apr 2026 22:39:47 +0200 Subject: [PATCH] refactor(compliance-sdk): split client/provider/embed/state under 500 LOC Phase 4 continuation. All touched files now under the file-size cap, and drive-by fixes unblock the types/core/react/vanilla builds which were broken at baseline. Splits - packages/types/src/state 505 -> 31 LOC barrel + state-flow/-assessment/-core - packages/core/src/client 521 -> 395 LOC + client-http 187 LOC (HTTP transport) - packages/react/src/provider 539 -> 460 LOC + provider-context 101 LOC - packages/vanilla/src/embed 611 -> 290 LOC + embed-banner 321 + embed-translations 78 Drive-by fixes (pre-existing typecheck/build failures) - types/rag.ts: rename colliding LegalDocument export to RagLegalDocument (the `export *` chain in index.ts was ambiguous; two consumers updated - core/modules/rag.ts drops unused import, vue/composables/useRAG.ts switches to the renamed symbol). - core/modules/rag.ts: wrap client searchRAG response to add the missing `query` field so the declared SearchResponse return type is satisfied. - react/provider.tsx: re-export useCompliance so ComplianceDashboard / ConsentBanner / DSRPortal legacy `from '../provider'` imports resolve. - vanilla/embed.ts + web-components/base.ts: default tenantId to '' so ComplianceClient construction typechecks. - vanilla/web-components/consent-banner.ts: tighten categories literal to `as const` so t.categories indexing narrows correctly. Verification: packages/types + core + react + vanilla all `pnpm build` clean with DTS emission. consent-sdk unaffected (still green). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../packages/core/src/client-http.ts | 187 +++++++ .../packages/core/src/client.ts | 302 +++------- .../packages/core/src/modules/rag.ts | 12 +- .../packages/react/src/provider-context.ts | 101 ++++ .../packages/react/src/provider.tsx | 105 +--- .../packages/types/src/rag.ts | 2 +- .../packages/types/src/state-assessment.ts | 52 ++ .../packages/types/src/state-core.ts | 161 ++++++ .../packages/types/src/state-flow.ts | 311 +++++++++++ .../packages/types/src/state.ts | 524 +----------------- .../packages/vanilla/src/embed-banner.ts | 321 +++++++++++ .../vanilla/src/embed-translations.ts | 78 +++ .../packages/vanilla/src/embed.ts | 378 +------------ .../vanilla/src/web-components/base.ts | 2 +- .../src/web-components/consent-banner.ts | 2 +- .../packages/vue/src/composables/useRAG.ts | 4 +- 16 files changed, 1380 insertions(+), 1162 deletions(-) create mode 100644 breakpilot-compliance-sdk/packages/core/src/client-http.ts create mode 100644 breakpilot-compliance-sdk/packages/react/src/provider-context.ts create mode 100644 breakpilot-compliance-sdk/packages/types/src/state-assessment.ts create mode 100644 breakpilot-compliance-sdk/packages/types/src/state-core.ts create mode 100644 breakpilot-compliance-sdk/packages/types/src/state-flow.ts create mode 100644 breakpilot-compliance-sdk/packages/vanilla/src/embed-banner.ts create mode 100644 breakpilot-compliance-sdk/packages/vanilla/src/embed-translations.ts diff --git a/breakpilot-compliance-sdk/packages/core/src/client-http.ts b/breakpilot-compliance-sdk/packages/core/src/client-http.ts new file mode 100644 index 0000000..8c287d5 --- /dev/null +++ b/breakpilot-compliance-sdk/packages/core/src/client-http.ts @@ -0,0 +1,187 @@ +/** + * HTTP transport primitives for ComplianceClient. + * + * Phase 4: extracted from client.ts. Handles headers, timeouts, retry logic, + * abort-controller lifecycle, and error classification so the main client + * module can focus on domain endpoints. + */ + +export interface HttpTransportOptions { + apiEndpoint: string + tenantId: string + timeout?: number + maxRetries?: number + onAuthError?: () => void + onError?: (error: Error) => void +} + +export interface APIError extends Error { + status?: number + code?: string + retryable: boolean +} + +export const DEFAULT_TIMEOUT = 30000 +export const DEFAULT_MAX_RETRIES = 3 +const RETRY_DELAYS = [1000, 2000, 4000] + +export function createHttpError( + message: string, + status?: number, + retryable = false +): APIError { + const error = new Error(message) as APIError + error.status = status + error.retryable = retryable + return error +} + +const sleep = (ms: number): Promise => + new Promise(resolve => setTimeout(resolve, ms)) + +/** + * HttpTransport — reusable fetch wrapper with retry + timeout + abort control. + * + * Not exposed via the SDK public surface; ComplianceClient instantiates one + * internally. + */ +export class HttpTransport { + readonly apiEndpoint: string + private apiKey: string | null = null + private accessToken: string | null = null + private tenantId: string + private timeout: number + private maxRetries: number + private abortControllers: Map = new Map() + private onAuthError?: () => void + private onError?: (error: Error) => void + + constructor(options: HttpTransportOptions) { + this.apiEndpoint = options.apiEndpoint.replace(/\/$/, '') + this.tenantId = options.tenantId + this.timeout = options.timeout ?? DEFAULT_TIMEOUT + this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES + this.onAuthError = options.onAuthError + this.onError = options.onError + } + + setApiKey(apiKey: string | null): void { + this.apiKey = apiKey + } + + setAccessToken(token: string | null): void { + this.accessToken = token + } + + setTenantId(tenantId: string): void { + this.tenantId = tenantId + } + + getTenantId(): string { + return this.tenantId + } + + 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 + } + + 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) + } + } + + 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 createHttpError(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 createHttpError(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 createHttpError('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 sleep(RETRY_DELAYS[attempt] || RETRY_DELAYS[RETRY_DELAYS.length - 1]) + } + } + + throw lastError || createHttpError('Unknown error', 500, false) + } + + cancelAllRequests(): void { + this.abortControllers.forEach(controller => controller.abort()) + this.abortControllers.clear() + } +} diff --git a/breakpilot-compliance-sdk/packages/core/src/client.ts b/breakpilot-compliance-sdk/packages/core/src/client.ts index e389db2..3d5bf88 100644 --- a/breakpilot-compliance-sdk/packages/core/src/client.ts +++ b/breakpilot-compliance-sdk/packages/core/src/client.ts @@ -1,8 +1,8 @@ /** * Compliance Client * - * Main entry point for the SDK. Handles API communication with - * retry logic, timeout handling, and optimistic locking. + * Main entry point for the SDK. Domain methods delegate to HttpTransport + * for retry/timeout/abort handling. Transport primitives live in client-http.ts. */ import type { @@ -19,6 +19,11 @@ import type { SDKState, CheckpointStatus, } from '@breakpilot/compliance-sdk-types' +import { + HttpTransport, + createHttpError, + type APIError, +} from './client-http' // ============================================================================= // TYPES @@ -34,157 +39,33 @@ export interface ComplianceClientOptions { 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 + private http: HttpTransport 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) + this.http = new HttpTransport({ + apiEndpoint: options.apiEndpoint, + tenantId: options.tenantId, + timeout: options.timeout, + maxRetries: options.maxRetries, + onError: options.onError, + onAuthError: options.onAuthError, + }) + if (options.apiKey) { + this.http.setApiKey(options.apiKey) } } - 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 get apiEndpoint(): string { + return this.http.apiEndpoint } - private sleep(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)) + private get tenantId(): string { + return this.http.getTenantId() } // --------------------------------------------------------------------------- @@ -192,7 +73,7 @@ export class ComplianceClient { // --------------------------------------------------------------------------- async authenticate(request: AuthTokenRequest): Promise { - const response = await this.fetchWithRetry>( + const response = await this.http.fetchWithRetry>( `${this.apiEndpoint}/auth/token`, { method: 'POST', @@ -202,11 +83,11 @@ export class ComplianceClient { ) if (response.success && response.data) { - this.accessToken = response.data.accessToken + this.http.setAccessToken(response.data.accessToken) return response.data } - throw this.createError(response.error || 'Authentication failed', 401, false) + throw createHttpError(response.error || 'Authentication failed', 401, false) } async refreshToken(refreshToken: string): Promise { @@ -218,11 +99,11 @@ export class ComplianceClient { } setAccessToken(token: string): void { - this.accessToken = token + this.http.setAccessToken(token) } clearAccessToken(): void { - this.accessToken = null + this.http.setAccessToken(null) } // --------------------------------------------------------------------------- @@ -231,11 +112,11 @@ export class ComplianceClient { async getState(): Promise { try { - const response = await this.fetchWithRetry>( + const response = await this.http.fetchWithRetry>( `${this.apiEndpoint}/state?tenantId=${encodeURIComponent(this.tenantId)}`, { method: 'GET', - headers: this.getHeaders(), + headers: this.http.getHeaders(), } ) @@ -254,12 +135,12 @@ export class ComplianceClient { } async saveState(state: SDKState, version?: number): Promise { - const response = await this.fetchWithRetry>( + const response = await this.http.fetchWithRetry>( `${this.apiEndpoint}/state`, { method: 'POST', headers: { - ...this.getHeaders(), + ...this.http.getHeaders(), ...(version !== undefined && { 'If-Match': String(version) }), }, body: JSON.stringify({ @@ -271,18 +152,18 @@ export class ComplianceClient { ) if (!response.success) { - throw this.createError(response.error || 'Failed to save state', 500, true) + throw createHttpError(response.error || 'Failed to save state', 500, true) } return response.data! } async deleteState(): Promise { - await this.fetchWithRetry>( + await this.http.fetchWithRetry>( `${this.apiEndpoint}/state?tenantId=${encodeURIComponent(this.tenantId)}`, { method: 'DELETE', - headers: this.getHeaders(), + headers: this.http.getHeaders(), } ) } @@ -295,34 +176,32 @@ export class ComplianceClient { 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, - }), - } - ) + const response = await this.http.fetchWithRetry< + APIResponse + >(`${this.apiEndpoint}/checkpoints/validate`, { + method: 'POST', + headers: this.http.getHeaders(), + body: JSON.stringify({ + tenantId: this.tenantId, + checkpointId, + data, + }), + }) if (!response.success || !response.data) { - throw this.createError(response.error || 'Checkpoint validation failed', 500, true) + throw createHttpError(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(), - } - ) + const response = await this.http.fetchWithRetry< + APIResponse> + >(`${this.apiEndpoint}/checkpoints?tenantId=${encodeURIComponent(this.tenantId)}`, { + method: 'GET', + headers: this.http.getHeaders(), + }) return response.data || {} } @@ -332,34 +211,34 @@ export class ComplianceClient { // --------------------------------------------------------------------------- async searchRAG(request: RAGSearchRequest): Promise { - const response = await this.fetchWithRetry>( + const response = await this.http.fetchWithRetry>( `${this.apiEndpoint}/rag/search`, { method: 'POST', - headers: this.getHeaders(), + headers: this.http.getHeaders(), body: JSON.stringify(request), } ) if (!response.success || !response.data) { - throw this.createError(response.error || 'RAG search failed', 500, true) + throw createHttpError(response.error || 'RAG search failed', 500, true) } return response.data } async askRAG(request: RAGAskRequest): Promise { - const response = await this.fetchWithRetry>( + const response = await this.http.fetchWithRetry>( `${this.apiEndpoint}/rag/ask`, { method: 'POST', - headers: this.getHeaders(), + headers: this.http.getHeaders(), body: JSON.stringify(request), } ) if (!response.success || !response.data) { - throw this.createError(response.error || 'RAG query failed', 500, true) + throw createHttpError(response.error || 'RAG query failed', 500, true) } return response.data @@ -370,12 +249,12 @@ export class ComplianceClient { // --------------------------------------------------------------------------- async exportState(format: ExportFormat): Promise { - const response = await this.fetchWithTimeout( + const response = await this.http.fetchWithTimeout( `${this.apiEndpoint}/export?tenantId=${encodeURIComponent(this.tenantId)}&format=${format}`, { method: 'GET', headers: { - ...this.getHeaders(), + ...this.http.getHeaders(), Accept: format === 'json' ? 'application/json' @@ -388,7 +267,7 @@ export class ComplianceClient { ) if (!response.ok) { - throw this.createError(`Export failed: ${response.statusText}`, response.status, true) + throw createHttpError(`Export failed: ${response.statusText}`, response.status, true) } return response.blob() @@ -402,22 +281,19 @@ export class ComplianceClient { type: 'dsfa' | 'tom' | 'vvt' | 'gutachten' | 'privacy_policy' | 'cookie_banner', options?: Record ): Promise<{ id: string; status: string; content?: string }> { - const response = await this.fetchWithRetry< + const response = await this.http.fetchWithRetry< APIResponse<{ id: string; status: string; content?: string }> - >( - `${this.apiEndpoint}/generate/${type}`, - { - method: 'POST', - headers: this.getHeaders(), - body: JSON.stringify({ - tenantId: this.tenantId, - options, - }), - } - ) + >(`${this.apiEndpoint}/generate/${type}`, { + method: 'POST', + headers: this.http.getHeaders(), + body: JSON.stringify({ + tenantId: this.tenantId, + options, + }), + }) if (!response.success || !response.data) { - throw this.createError(response.error || 'Document generation failed', 500, true) + throw createHttpError(response.error || 'Document generation failed', 500, true) } return response.data @@ -433,31 +309,30 @@ export class ComplianceClient { 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, - }), - } - ) + const response = await this.http.fetchWithRetry< + APIResponse<{ id: string; status: string }> + >(`${this.apiEndpoint}/security/scan`, { + method: 'POST', + headers: this.http.getHeaders(), + body: JSON.stringify({ + tenantId: this.tenantId, + ...options, + }), + }) if (!response.success || !response.data) { - throw this.createError(response.error || 'Security scan failed', 500, true) + throw createHttpError(response.error || 'Security scan failed', 500, true) } return response.data } async getSecurityScanResult(scanId: string): Promise { - const response = await this.fetchWithRetry>( + const response = await this.http.fetchWithRetry>( `${this.apiEndpoint}/security/scan/${scanId}`, { method: 'GET', - headers: this.getHeaders(), + headers: this.http.getHeaders(), } ) @@ -469,21 +344,20 @@ export class ComplianceClient { // --------------------------------------------------------------------------- cancelAllRequests(): void { - this.abortControllers.forEach(controller => controller.abort()) - this.abortControllers.clear() + this.http.cancelAllRequests() } setTenantId(tenantId: string): void { - this.tenantId = tenantId + this.http.setTenantId(tenantId) } getTenantId(): string { - return this.tenantId + return this.http.getTenantId() } async healthCheck(): Promise { try { - const response = await this.fetchWithTimeout( + const response = await this.http.fetchWithTimeout( `${this.apiEndpoint}/health`, { method: 'GET' }, `health-${Date.now()}` diff --git a/breakpilot-compliance-sdk/packages/core/src/modules/rag.ts b/breakpilot-compliance-sdk/packages/core/src/modules/rag.ts index 1d160f4..b917fdf 100644 --- a/breakpilot-compliance-sdk/packages/core/src/modules/rag.ts +++ b/breakpilot-compliance-sdk/packages/core/src/modules/rag.ts @@ -9,8 +9,6 @@ import type { SearchResponse, AssistantQuery, AssistantResponse, - LegalDocument, - ChatSession, ChatMessage, } from '@breakpilot/compliance-sdk-types' import { ComplianceClient } from '../client' @@ -38,12 +36,20 @@ export class RAGModule { scoreThreshold: options?.scoreThreshold ?? 0.5, } - return this.client.searchRAG({ + const response = await this.client.searchRAG({ query: searchRequest.query, filters: searchRequest.filters, limit: searchRequest.limit, offset: searchRequest.offset, }) + + // The client returns RAGSearchResponse (api.ts shape); enrich it with + // the originating query so the consumer-facing SearchResponse shape + // from types/rag.ts is satisfied. + return { + ...response, + query: searchRequest.query, + } as unknown as SearchResponse } async searchByRegulation(regulation: string, query: string): Promise { diff --git a/breakpilot-compliance-sdk/packages/react/src/provider-context.ts b/breakpilot-compliance-sdk/packages/react/src/provider-context.ts new file mode 100644 index 0000000..f67174a --- /dev/null +++ b/breakpilot-compliance-sdk/packages/react/src/provider-context.ts @@ -0,0 +1,101 @@ +/** + * ComplianceContext — React context + value shape for ComplianceProvider. + * + * Phase 4: extracted from provider.tsx so the provider module stays under + * the file-size cap. + */ + +import { createContext } from 'react' +import type { + ComplianceClient, + DSGVOModule, + ComplianceModule, + RAGModule, + SecurityModule, +} from '@breakpilot/compliance-sdk-core' +import type { + SDKState, + SDKAction, + SDKStep, + CheckpointStatus, + SyncState, + UseCaseAssessment, + Risk, + Control, +} from '@breakpilot/compliance-sdk-types' + +// Shared localStorage key prefix for provider persistence. +export const SDK_STORAGE_KEY = 'breakpilot-compliance-sdk-state' + +export interface ComplianceContextValue { + // State + state: SDKState + dispatch: React.Dispatch + + // Client + client: ComplianceClient + + // Modules + dsgvo: DSGVOModule + compliance: ComplianceModule + rag: RAGModule + security: SecurityModule + + // Navigation + currentStep: SDKStep | undefined + goToStep: (stepId: string) => void + goToNextStep: () => void + goToPreviousStep: () => void + canGoNext: boolean + canGoPrevious: boolean + + // Progress + completionPercentage: number + phase1Completion: number + phase2Completion: number + + // Checkpoints + validateCheckpoint: (checkpointId: string) => Promise + overrideCheckpoint: (checkpointId: string, reason: string) => Promise + getCheckpointStatus: (checkpointId: string) => CheckpointStatus | undefined + + // State Updates + updateUseCase: (id: string, data: Partial) => void + addRisk: (risk: Risk) => void + updateControl: (id: string, data: Partial) => void + + // Persistence + saveState: () => Promise + loadState: () => Promise + resetState: () => void + + // Sync + syncState: SyncState + forceSyncToServer: () => Promise + isOnline: boolean + + // Export + exportState: (format: 'json' | 'pdf' | 'zip') => Promise + + // Command Bar + isCommandBarOpen: boolean + setCommandBarOpen: (open: boolean) => void + + // Status + isInitialized: boolean + isLoading: boolean + error: Error | null +} + +export const ComplianceContext = createContext(null) + +export interface ComplianceProviderProps { + children: React.ReactNode + apiEndpoint: string + apiKey?: string + tenantId: string + userId?: string + enableBackendSync?: boolean + onNavigate?: (url: string) => void + onError?: (error: Error) => void +} diff --git a/breakpilot-compliance-sdk/packages/react/src/provider.tsx b/breakpilot-compliance-sdk/packages/react/src/provider.tsx index 89004be..9ff3d12 100644 --- a/breakpilot-compliance-sdk/packages/react/src/provider.tsx +++ b/breakpilot-compliance-sdk/packages/react/src/provider.tsx @@ -1,8 +1,6 @@ 'use client' import React, { - createContext, - useContext, useReducer, useEffect, useCallback, @@ -20,21 +18,13 @@ import { createComplianceModule, createRAGModule, createSecurityModule, - type DSGVOModule, - type ComplianceModule, - type RAGModule, - type SecurityModule, } from '@breakpilot/compliance-sdk-core' import type { - SDKState, - SDKAction, - SDKStep, CheckpointStatus, SyncState, UseCaseAssessment, Risk, Control, - UserPreferences, } from '@breakpilot/compliance-sdk-types' import { getStepById, @@ -43,94 +33,27 @@ import { getCompletionPercentage, getPhaseCompletionPercentage, } from '@breakpilot/compliance-sdk-types' +import { + ComplianceContext, + SDK_STORAGE_KEY, + type ComplianceContextValue, + type ComplianceProviderProps, +} from './provider-context' -// ============================================================================= -// CONTEXT TYPES -// ============================================================================= +export { + ComplianceContext, + type ComplianceContextValue, + type ComplianceProviderProps, +} from './provider-context' -export interface ComplianceContextValue { - // State - state: SDKState - dispatch: React.Dispatch - - // Client - client: ComplianceClient - - // Modules - dsgvo: DSGVOModule - compliance: ComplianceModule - rag: RAGModule - security: SecurityModule - - // Navigation - currentStep: SDKStep | undefined - goToStep: (stepId: string) => void - goToNextStep: () => void - goToPreviousStep: () => void - canGoNext: boolean - canGoPrevious: boolean - - // Progress - completionPercentage: number - phase1Completion: number - phase2Completion: number - - // Checkpoints - validateCheckpoint: (checkpointId: string) => Promise - overrideCheckpoint: (checkpointId: string, reason: string) => Promise - getCheckpointStatus: (checkpointId: string) => CheckpointStatus | undefined - - // State Updates - updateUseCase: (id: string, data: Partial) => void - addRisk: (risk: Risk) => void - updateControl: (id: string, data: Partial) => void - - // Persistence - saveState: () => Promise - loadState: () => Promise - resetState: () => void - - // Sync - syncState: SyncState - forceSyncToServer: () => Promise - isOnline: boolean - - // Export - exportState: (format: 'json' | 'pdf' | 'zip') => Promise - - // Command Bar - isCommandBarOpen: boolean - setCommandBarOpen: (open: boolean) => void - - // Status - isInitialized: boolean - isLoading: boolean - error: Error | null -} - -export const ComplianceContext = createContext(null) - -// ============================================================================= -// PROVIDER PROPS -// ============================================================================= - -export interface ComplianceProviderProps { - children: React.ReactNode - apiEndpoint: string - apiKey?: string - tenantId: string - userId?: string - enableBackendSync?: boolean - onNavigate?: (url: string) => void - onError?: (error: Error) => void -} +// Re-export useCompliance so legacy component imports (`from '../provider'`) +// keep resolving. Pre-existing cross-file import that was broken in baseline. +export { useCompliance } from './hooks' // ============================================================================= // PROVIDER // ============================================================================= -const SDK_STORAGE_KEY = 'breakpilot-compliance-sdk-state' - export function ComplianceProvider({ children, apiEndpoint, diff --git a/breakpilot-compliance-sdk/packages/types/src/rag.ts b/breakpilot-compliance-sdk/packages/types/src/rag.ts index 36974b2..031de39 100644 --- a/breakpilot-compliance-sdk/packages/types/src/rag.ts +++ b/breakpilot-compliance-sdk/packages/types/src/rag.ts @@ -14,7 +14,7 @@ import type { RegulationCode } from './compliance' // LEGAL CORPUS // ============================================================================= -export interface LegalDocument { +export interface RagLegalDocument { id: string code: RegulationCode | string name: string diff --git a/breakpilot-compliance-sdk/packages/types/src/state-assessment.ts b/breakpilot-compliance-sdk/packages/types/src/state-assessment.ts new file mode 100644 index 0000000..86b49b0 --- /dev/null +++ b/breakpilot-compliance-sdk/packages/types/src/state-assessment.ts @@ -0,0 +1,52 @@ +/** + * Use-case assessment + screening result types. + * + * Phase 4: extracted from state.ts. + */ + +import type { SBOM, SecurityScanResult } from './security' + +// ============================================================================= +// USE CASE ASSESSMENT +// ============================================================================= + +export interface UseCaseStep { + id: string + name: string + completed: boolean + data: Record +} + +export interface AssessmentResult { + riskLevel: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL' + applicableRegulations: string[] + recommendedControls: string[] + dsfaRequired: boolean + aiActClassification: string +} + +export interface UseCaseAssessment { + id: string + name: string + description: string + category: string + stepsCompleted: number + steps: UseCaseStep[] + assessmentResult: AssessmentResult | null + createdAt: Date + updatedAt: Date +} + +// ============================================================================= +// SCREENING RESULT +// ============================================================================= + +export interface ScreeningResult { + id: string + status: 'PENDING' | 'RUNNING' | 'COMPLETED' | 'FAILED' + startedAt: Date + completedAt: Date | null + sbom: SBOM | null + securityScan: SecurityScanResult | null + error: string | null +} diff --git a/breakpilot-compliance-sdk/packages/types/src/state-core.ts b/breakpilot-compliance-sdk/packages/types/src/state-core.ts new file mode 100644 index 0000000..bc8c106 --- /dev/null +++ b/breakpilot-compliance-sdk/packages/types/src/state-core.ts @@ -0,0 +1,161 @@ +/** + * SDKState interface + SDKAction union + completion-percentage helpers. + * + * Phase 4: extracted from state.ts. + */ + +import type { + SubscriptionTier, + UserPreferences, + Risk, + CommandHistory, + CheckpointStatus, +} from './base' +import type { + ConsentRecord, + DSRConfig, + DSRRequest, + CookieBannerConfig, + ProcessingActivity, + DSFA, + TOM, + RetentionPolicy, + EscalationWorkflow, + LegalDocument, +} from './dsgvo' +import type { + Control, + Evidence, + Requirement, + ServiceModule, + Obligation, + AIActResult, + ChecklistItem, +} from './compliance' +import type { SBOM, SecurityIssue, BacklogItem } from './security' +import type { UseCaseAssessment, ScreeningResult } from './state-assessment' +import { SDK_STEPS, getStepsForPhase, type SDKPhase } from './state-flow' + +// ============================================================================= +// SDK STATE +// ============================================================================= + +export interface SDKState { + // Metadata + version: string + lastModified: Date + + // Tenant & User + tenantId: string + userId: string + subscription: SubscriptionTier + + // Progress + currentPhase: SDKPhase + currentStep: string + completedSteps: string[] + checkpoints: Record + + // Phase 1 Data + useCases: UseCaseAssessment[] + activeUseCase: string | null + screening: ScreeningResult | null + modules: ServiceModule[] + requirements: Requirement[] + controls: Control[] + evidence: Evidence[] + checklist: ChecklistItem[] + risks: Risk[] + + // Phase 2 Data + aiActClassification: AIActResult | null + obligations: Obligation[] + dsfa: DSFA | null + toms: TOM[] + retentionPolicies: RetentionPolicy[] + vvt: ProcessingActivity[] + documents: LegalDocument[] + cookieBanner: CookieBannerConfig | null + consents: ConsentRecord[] + dsrConfig: DSRConfig | null + dsrRequests: DSRRequest[] + escalationWorkflows: EscalationWorkflow[] + + // Security + sbom: SBOM | null + securityIssues: SecurityIssue[] + securityBacklog: BacklogItem[] + + // UI State + commandBarHistory: CommandHistory[] + recentSearches: string[] + preferences: UserPreferences +} + +// ============================================================================= +// SDK ACTIONS +// ============================================================================= + +export type SDKAction = + | { type: 'SET_STATE'; payload: Partial } + | { type: 'SET_CURRENT_STEP'; payload: string } + | { type: 'COMPLETE_STEP'; payload: string } + | { type: 'SET_CHECKPOINT_STATUS'; payload: { id: string; status: CheckpointStatus } } + | { type: 'ADD_USE_CASE'; payload: UseCaseAssessment } + | { type: 'UPDATE_USE_CASE'; payload: { id: string; data: Partial } } + | { type: 'DELETE_USE_CASE'; payload: string } + | { type: 'SET_ACTIVE_USE_CASE'; payload: string | null } + | { type: 'SET_SCREENING'; payload: ScreeningResult } + | { type: 'ADD_MODULE'; payload: ServiceModule } + | { type: 'UPDATE_MODULE'; payload: { id: string; data: Partial } } + | { type: 'ADD_REQUIREMENT'; payload: Requirement } + | { type: 'UPDATE_REQUIREMENT'; payload: { id: string; data: Partial } } + | { type: 'ADD_CONTROL'; payload: Control } + | { type: 'UPDATE_CONTROL'; payload: { id: string; data: Partial } } + | { type: 'ADD_EVIDENCE'; payload: Evidence } + | { type: 'UPDATE_EVIDENCE'; payload: { id: string; data: Partial } } + | { type: 'DELETE_EVIDENCE'; payload: string } + | { type: 'ADD_RISK'; payload: Risk } + | { type: 'UPDATE_RISK'; payload: { id: string; data: Partial } } + | { type: 'DELETE_RISK'; payload: string } + | { type: 'SET_AI_ACT_RESULT'; payload: AIActResult } + | { type: 'ADD_OBLIGATION'; payload: Obligation } + | { type: 'UPDATE_OBLIGATION'; payload: { id: string; data: Partial } } + | { type: 'SET_DSFA'; payload: DSFA } + | { type: 'ADD_TOM'; payload: TOM } + | { type: 'UPDATE_TOM'; payload: { id: string; data: Partial } } + | { type: 'ADD_RETENTION_POLICY'; payload: RetentionPolicy } + | { type: 'UPDATE_RETENTION_POLICY'; payload: { id: string; data: Partial } } + | { type: 'ADD_PROCESSING_ACTIVITY'; payload: ProcessingActivity } + | { type: 'UPDATE_PROCESSING_ACTIVITY'; payload: { id: string; data: Partial } } + | { type: 'ADD_DOCUMENT'; payload: LegalDocument } + | { type: 'UPDATE_DOCUMENT'; payload: { id: string; data: Partial } } + | { type: 'SET_COOKIE_BANNER'; payload: CookieBannerConfig } + | { type: 'SET_DSR_CONFIG'; payload: DSRConfig } + | { type: 'ADD_DSR_REQUEST'; payload: DSRRequest } + | { type: 'UPDATE_DSR_REQUEST'; payload: { id: string; data: Partial } } + | { type: 'ADD_ESCALATION_WORKFLOW'; payload: EscalationWorkflow } + | { type: 'UPDATE_ESCALATION_WORKFLOW'; payload: { id: string; data: Partial } } + | { type: 'ADD_SECURITY_ISSUE'; payload: SecurityIssue } + | { type: 'UPDATE_SECURITY_ISSUE'; payload: { id: string; data: Partial } } + | { type: 'ADD_BACKLOG_ITEM'; payload: BacklogItem } + | { type: 'UPDATE_BACKLOG_ITEM'; payload: { id: string; data: Partial } } + | { type: 'ADD_COMMAND_HISTORY'; payload: CommandHistory } + | { type: 'SET_PREFERENCES'; payload: Partial } + | { type: 'RESET_STATE' } + +// ============================================================================= +// COMPLETION HELPERS +// ============================================================================= + +export function getCompletionPercentage(state: SDKState): number { + const totalSteps = SDK_STEPS.length + const completedSteps = state.completedSteps.length + return Math.round((completedSteps / totalSteps) * 100) +} + +export function getPhaseCompletionPercentage(state: SDKState, phase: SDKPhase): number { + const phaseSteps = getStepsForPhase(phase) + const completedPhaseSteps = phaseSteps.filter(s => state.completedSteps.includes(s.id)) + return Math.round((completedPhaseSteps.length / phaseSteps.length) * 100) +} diff --git a/breakpilot-compliance-sdk/packages/types/src/state-flow.ts b/breakpilot-compliance-sdk/packages/types/src/state-flow.ts new file mode 100644 index 0000000..297223c --- /dev/null +++ b/breakpilot-compliance-sdk/packages/types/src/state-flow.ts @@ -0,0 +1,311 @@ +/** + * SDK flow definition — phase/step types + ordered step list + navigation helpers. + * + * Phase 4: extracted from state.ts so the root state module stays under the + * file-size cap. + */ + +// ============================================================================= +// SDK FLOW & NAVIGATION +// ============================================================================= + +export type SDKPhase = 1 | 2 + +export interface SDKStep { + id: string + phase: SDKPhase + order: number + name: string + nameShort: string + description: string + url: string + checkpointId: string + prerequisiteSteps: string[] + isOptional: boolean +} + +export const SDK_STEPS: SDKStep[] = [ + // Phase 1: Automatisches Compliance Assessment + { + id: 'use-case-workshop', + phase: 1, + order: 1, + name: 'Use Case Workshop', + nameShort: 'Use Case', + description: '5-Schritte-Wizard für Use Case Erfassung', + url: '/sdk/advisory-board', + checkpointId: 'CP-UC', + prerequisiteSteps: [], + isOptional: false, + }, + { + id: 'screening', + phase: 1, + order: 2, + name: 'System Screening', + nameShort: 'Screening', + description: 'SBOM + Security Check', + url: '/sdk/screening', + checkpointId: 'CP-SCAN', + prerequisiteSteps: ['use-case-workshop'], + isOptional: false, + }, + { + id: 'modules', + phase: 1, + order: 3, + name: 'Compliance Modules', + nameShort: 'Module', + description: 'Abgleich welche Regulierungen gelten', + url: '/sdk/modules', + checkpointId: 'CP-MOD', + prerequisiteSteps: ['screening'], + isOptional: false, + }, + { + id: 'requirements', + phase: 1, + order: 4, + name: 'Requirements', + nameShort: 'Anforderungen', + description: 'Prüfaspekte aus Regulierungen ableiten', + url: '/sdk/requirements', + checkpointId: 'CP-REQ', + prerequisiteSteps: ['modules'], + isOptional: false, + }, + { + id: 'controls', + phase: 1, + order: 5, + name: 'Controls', + nameShort: 'Controls', + description: 'Erforderliche Maßnahmen ermitteln', + url: '/sdk/controls', + checkpointId: 'CP-CTRL', + prerequisiteSteps: ['requirements'], + isOptional: false, + }, + { + id: 'evidence', + phase: 1, + order: 6, + name: 'Evidence', + nameShort: 'Nachweise', + description: 'Nachweise dokumentieren', + url: '/sdk/evidence', + checkpointId: 'CP-EVI', + prerequisiteSteps: ['controls'], + isOptional: false, + }, + { + id: 'audit-checklist', + phase: 1, + order: 7, + name: 'Audit Checklist', + nameShort: 'Checklist', + description: 'Prüfliste generieren', + url: '/sdk/audit-checklist', + checkpointId: 'CP-CHK', + prerequisiteSteps: ['evidence'], + isOptional: false, + }, + { + id: 'risks', + phase: 1, + order: 8, + name: 'Risk Matrix', + nameShort: 'Risiken', + description: 'Risikobewertung & Residual Risk', + url: '/sdk/risks', + checkpointId: 'CP-RISK', + prerequisiteSteps: ['audit-checklist'], + isOptional: false, + }, + // Phase 2: Dokumentengenerierung + { + id: 'ai-act', + phase: 2, + order: 1, + name: 'AI Act Klassifizierung', + nameShort: 'AI Act', + description: 'Risikostufe nach EU AI Act', + url: '/sdk/ai-act', + checkpointId: 'CP-AI', + prerequisiteSteps: ['risks'], + isOptional: false, + }, + { + id: 'obligations', + phase: 2, + order: 2, + name: 'Pflichtenübersicht', + nameShort: 'Pflichten', + description: 'NIS2, DSGVO, AI Act Pflichten', + url: '/sdk/obligations', + checkpointId: 'CP-OBL', + prerequisiteSteps: ['ai-act'], + isOptional: false, + }, + { + id: 'dsfa', + phase: 2, + order: 3, + name: 'DSFA', + nameShort: 'DSFA', + description: 'Datenschutz-Folgenabschätzung', + url: '/sdk/dsfa', + checkpointId: 'CP-DSFA', + prerequisiteSteps: ['obligations'], + isOptional: true, + }, + { + id: 'tom', + phase: 2, + order: 4, + name: 'TOMs', + nameShort: 'TOMs', + description: 'Technische & Org. Maßnahmen', + url: '/sdk/tom', + checkpointId: 'CP-TOM', + prerequisiteSteps: ['dsfa'], + isOptional: false, + }, + { + id: 'loeschfristen', + phase: 2, + order: 5, + name: 'Löschfristen', + nameShort: 'Löschfristen', + description: 'Aufbewahrungsrichtlinien', + url: '/sdk/loeschfristen', + checkpointId: 'CP-RET', + prerequisiteSteps: ['tom'], + isOptional: false, + }, + { + id: 'vvt', + phase: 2, + order: 6, + name: 'Verarbeitungsverzeichnis', + nameShort: 'VVT', + description: 'Art. 30 DSGVO Dokumentation', + url: '/sdk/vvt', + checkpointId: 'CP-VVT', + prerequisiteSteps: ['loeschfristen'], + isOptional: false, + }, + { + id: 'consent', + phase: 2, + order: 7, + name: 'Rechtliche Vorlagen', + nameShort: 'Vorlagen', + description: 'AGB, Datenschutz, Nutzungsbedingungen', + url: '/sdk/consent', + checkpointId: 'CP-DOC', + prerequisiteSteps: ['vvt'], + isOptional: false, + }, + { + id: 'cookie-banner', + phase: 2, + order: 8, + name: 'Cookie Banner', + nameShort: 'Cookies', + description: 'Cookie-Consent Generator', + url: '/sdk/cookie-banner', + checkpointId: 'CP-COOK', + prerequisiteSteps: ['consent'], + isOptional: false, + }, + { + id: 'einwilligungen', + phase: 2, + order: 9, + name: 'Einwilligungen', + nameShort: 'Consents', + description: 'Consent-Tracking Setup', + url: '/sdk/einwilligungen', + checkpointId: 'CP-CONS', + prerequisiteSteps: ['cookie-banner'], + isOptional: false, + }, + { + id: 'dsr', + phase: 2, + order: 10, + name: 'DSR Portal', + nameShort: 'DSR', + description: 'Betroffenenrechte-Portal', + url: '/sdk/dsr', + checkpointId: 'CP-DSR', + prerequisiteSteps: ['einwilligungen'], + isOptional: false, + }, + { + id: 'escalations', + phase: 2, + order: 11, + name: 'Escalations', + nameShort: 'Eskalationen', + description: 'Management-Workflows', + url: '/sdk/escalations', + checkpointId: 'CP-ESC', + prerequisiteSteps: ['dsr'], + isOptional: false, + }, +] + +// ============================================================================= +// HELPER FUNCTIONS +// ============================================================================= + +export function getStepById(stepId: string): SDKStep | undefined { + return SDK_STEPS.find(s => s.id === stepId) +} + +export function getStepByUrl(url: string): SDKStep | undefined { + return SDK_STEPS.find(s => s.url === url) +} + +export function getStepsForPhase(phase: SDKPhase): SDKStep[] { + return SDK_STEPS.filter(s => s.phase === phase).sort((a, b) => a.order - b.order) +} + +export function getNextStep(currentStepId: string): SDKStep | undefined { + const currentStep = getStepById(currentStepId) + if (!currentStep) return undefined + + const stepsInPhase = getStepsForPhase(currentStep.phase) + const currentIndex = stepsInPhase.findIndex(s => s.id === currentStepId) + + if (currentIndex < stepsInPhase.length - 1) { + return stepsInPhase[currentIndex + 1] + } + + if (currentStep.phase === 1) { + return getStepsForPhase(2)[0] + } + + return undefined +} + +export function getPreviousStep(currentStepId: string): SDKStep | undefined { + const currentStep = getStepById(currentStepId) + if (!currentStep) return undefined + + const stepsInPhase = getStepsForPhase(currentStep.phase) + const currentIndex = stepsInPhase.findIndex(s => s.id === currentStepId) + + if (currentIndex > 0) { + return stepsInPhase[currentIndex - 1] + } + + if (currentStep.phase === 2) { + const phase1Steps = getStepsForPhase(1) + return phase1Steps[phase1Steps.length - 1] + } + + return undefined +} diff --git a/breakpilot-compliance-sdk/packages/types/src/state.ts b/breakpilot-compliance-sdk/packages/types/src/state.ts index f9d8ebb..b7c2cfd 100644 --- a/breakpilot-compliance-sdk/packages/types/src/state.ts +++ b/breakpilot-compliance-sdk/packages/types/src/state.ts @@ -1,505 +1,31 @@ /** - * SDK State Types + * SDK State Types — barrel re-export. * - * Type definitions for the SDK's state management system + * Phase 4: flow/step data, assessment types, and SDKState/SDKAction live in + * sibling files to keep each module under the file-size cap. */ -import type { - SubscriptionTier, - UserPreferences, - Risk, - CommandHistory, - CheckpointStatus, -} from './base' -import type { - ConsentRecord, - DSRConfig, - DSRRequest, - CookieBannerConfig, - ProcessingActivity, - DSFA, - TOM, - RetentionPolicy, - EscalationWorkflow, - LegalDocument, -} from './dsgvo' -import type { - Control, - Evidence, - Requirement, - ServiceModule, - Obligation, - AIActResult, - ChecklistItem, -} from './compliance' -import type { SBOM, SecurityIssue, BacklogItem, SecurityScanResult } from './security' +export { + SDK_STEPS, + getStepById, + getStepByUrl, + getStepsForPhase, + getNextStep, + getPreviousStep, + type SDKPhase, + type SDKStep, +} from './state-flow' -// ============================================================================= -// SDK FLOW & NAVIGATION -// ============================================================================= +export type { + UseCaseStep, + AssessmentResult, + UseCaseAssessment, + ScreeningResult, +} from './state-assessment' -export type SDKPhase = 1 | 2 - -export interface SDKStep { - id: string - phase: SDKPhase - order: number - name: string - nameShort: string - description: string - url: string - checkpointId: string - prerequisiteSteps: string[] - isOptional: boolean -} - -export const SDK_STEPS: SDKStep[] = [ - // Phase 1: Automatisches Compliance Assessment - { - id: 'use-case-workshop', - phase: 1, - order: 1, - name: 'Use Case Workshop', - nameShort: 'Use Case', - description: '5-Schritte-Wizard für Use Case Erfassung', - url: '/sdk/advisory-board', - checkpointId: 'CP-UC', - prerequisiteSteps: [], - isOptional: false, - }, - { - id: 'screening', - phase: 1, - order: 2, - name: 'System Screening', - nameShort: 'Screening', - description: 'SBOM + Security Check', - url: '/sdk/screening', - checkpointId: 'CP-SCAN', - prerequisiteSteps: ['use-case-workshop'], - isOptional: false, - }, - { - id: 'modules', - phase: 1, - order: 3, - name: 'Compliance Modules', - nameShort: 'Module', - description: 'Abgleich welche Regulierungen gelten', - url: '/sdk/modules', - checkpointId: 'CP-MOD', - prerequisiteSteps: ['screening'], - isOptional: false, - }, - { - id: 'requirements', - phase: 1, - order: 4, - name: 'Requirements', - nameShort: 'Anforderungen', - description: 'Prüfaspekte aus Regulierungen ableiten', - url: '/sdk/requirements', - checkpointId: 'CP-REQ', - prerequisiteSteps: ['modules'], - isOptional: false, - }, - { - id: 'controls', - phase: 1, - order: 5, - name: 'Controls', - nameShort: 'Controls', - description: 'Erforderliche Maßnahmen ermitteln', - url: '/sdk/controls', - checkpointId: 'CP-CTRL', - prerequisiteSteps: ['requirements'], - isOptional: false, - }, - { - id: 'evidence', - phase: 1, - order: 6, - name: 'Evidence', - nameShort: 'Nachweise', - description: 'Nachweise dokumentieren', - url: '/sdk/evidence', - checkpointId: 'CP-EVI', - prerequisiteSteps: ['controls'], - isOptional: false, - }, - { - id: 'audit-checklist', - phase: 1, - order: 7, - name: 'Audit Checklist', - nameShort: 'Checklist', - description: 'Prüfliste generieren', - url: '/sdk/audit-checklist', - checkpointId: 'CP-CHK', - prerequisiteSteps: ['evidence'], - isOptional: false, - }, - { - id: 'risks', - phase: 1, - order: 8, - name: 'Risk Matrix', - nameShort: 'Risiken', - description: 'Risikobewertung & Residual Risk', - url: '/sdk/risks', - checkpointId: 'CP-RISK', - prerequisiteSteps: ['audit-checklist'], - isOptional: false, - }, - // Phase 2: Dokumentengenerierung - { - id: 'ai-act', - phase: 2, - order: 1, - name: 'AI Act Klassifizierung', - nameShort: 'AI Act', - description: 'Risikostufe nach EU AI Act', - url: '/sdk/ai-act', - checkpointId: 'CP-AI', - prerequisiteSteps: ['risks'], - isOptional: false, - }, - { - id: 'obligations', - phase: 2, - order: 2, - name: 'Pflichtenübersicht', - nameShort: 'Pflichten', - description: 'NIS2, DSGVO, AI Act Pflichten', - url: '/sdk/obligations', - checkpointId: 'CP-OBL', - prerequisiteSteps: ['ai-act'], - isOptional: false, - }, - { - id: 'dsfa', - phase: 2, - order: 3, - name: 'DSFA', - nameShort: 'DSFA', - description: 'Datenschutz-Folgenabschätzung', - url: '/sdk/dsfa', - checkpointId: 'CP-DSFA', - prerequisiteSteps: ['obligations'], - isOptional: true, - }, - { - id: 'tom', - phase: 2, - order: 4, - name: 'TOMs', - nameShort: 'TOMs', - description: 'Technische & Org. Maßnahmen', - url: '/sdk/tom', - checkpointId: 'CP-TOM', - prerequisiteSteps: ['dsfa'], - isOptional: false, - }, - { - id: 'loeschfristen', - phase: 2, - order: 5, - name: 'Löschfristen', - nameShort: 'Löschfristen', - description: 'Aufbewahrungsrichtlinien', - url: '/sdk/loeschfristen', - checkpointId: 'CP-RET', - prerequisiteSteps: ['tom'], - isOptional: false, - }, - { - id: 'vvt', - phase: 2, - order: 6, - name: 'Verarbeitungsverzeichnis', - nameShort: 'VVT', - description: 'Art. 30 DSGVO Dokumentation', - url: '/sdk/vvt', - checkpointId: 'CP-VVT', - prerequisiteSteps: ['loeschfristen'], - isOptional: false, - }, - { - id: 'consent', - phase: 2, - order: 7, - name: 'Rechtliche Vorlagen', - nameShort: 'Vorlagen', - description: 'AGB, Datenschutz, Nutzungsbedingungen', - url: '/sdk/consent', - checkpointId: 'CP-DOC', - prerequisiteSteps: ['vvt'], - isOptional: false, - }, - { - id: 'cookie-banner', - phase: 2, - order: 8, - name: 'Cookie Banner', - nameShort: 'Cookies', - description: 'Cookie-Consent Generator', - url: '/sdk/cookie-banner', - checkpointId: 'CP-COOK', - prerequisiteSteps: ['consent'], - isOptional: false, - }, - { - id: 'einwilligungen', - phase: 2, - order: 9, - name: 'Einwilligungen', - nameShort: 'Consents', - description: 'Consent-Tracking Setup', - url: '/sdk/einwilligungen', - checkpointId: 'CP-CONS', - prerequisiteSteps: ['cookie-banner'], - isOptional: false, - }, - { - id: 'dsr', - phase: 2, - order: 10, - name: 'DSR Portal', - nameShort: 'DSR', - description: 'Betroffenenrechte-Portal', - url: '/sdk/dsr', - checkpointId: 'CP-DSR', - prerequisiteSteps: ['einwilligungen'], - isOptional: false, - }, - { - id: 'escalations', - phase: 2, - order: 11, - name: 'Escalations', - nameShort: 'Eskalationen', - description: 'Management-Workflows', - url: '/sdk/escalations', - checkpointId: 'CP-ESC', - prerequisiteSteps: ['dsr'], - isOptional: false, - }, -] - -// ============================================================================= -// USE CASE ASSESSMENT -// ============================================================================= - -export interface UseCaseStep { - id: string - name: string - completed: boolean - data: Record -} - -export interface AssessmentResult { - riskLevel: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL' - applicableRegulations: string[] - recommendedControls: string[] - dsfaRequired: boolean - aiActClassification: string -} - -export interface UseCaseAssessment { - id: string - name: string - description: string - category: string - stepsCompleted: number - steps: UseCaseStep[] - assessmentResult: AssessmentResult | null - createdAt: Date - updatedAt: Date -} - -// ============================================================================= -// SCREENING RESULT -// ============================================================================= - -export interface ScreeningResult { - id: string - status: 'PENDING' | 'RUNNING' | 'COMPLETED' | 'FAILED' - startedAt: Date - completedAt: Date | null - sbom: SBOM | null - securityScan: SecurityScanResult | null - error: string | null -} - -// ============================================================================= -// SDK STATE -// ============================================================================= - -export interface SDKState { - // Metadata - version: string - lastModified: Date - - // Tenant & User - tenantId: string - userId: string - subscription: SubscriptionTier - - // Progress - currentPhase: SDKPhase - currentStep: string - completedSteps: string[] - checkpoints: Record - - // Phase 1 Data - useCases: UseCaseAssessment[] - activeUseCase: string | null - screening: ScreeningResult | null - modules: ServiceModule[] - requirements: Requirement[] - controls: Control[] - evidence: Evidence[] - checklist: ChecklistItem[] - risks: Risk[] - - // Phase 2 Data - aiActClassification: AIActResult | null - obligations: Obligation[] - dsfa: DSFA | null - toms: TOM[] - retentionPolicies: RetentionPolicy[] - vvt: ProcessingActivity[] - documents: LegalDocument[] - cookieBanner: CookieBannerConfig | null - consents: ConsentRecord[] - dsrConfig: DSRConfig | null - dsrRequests: DSRRequest[] - escalationWorkflows: EscalationWorkflow[] - - // Security - sbom: SBOM | null - securityIssues: SecurityIssue[] - securityBacklog: BacklogItem[] - - // UI State - commandBarHistory: CommandHistory[] - recentSearches: string[] - preferences: UserPreferences -} - -// ============================================================================= -// SDK ACTIONS -// ============================================================================= - -export type SDKAction = - | { type: 'SET_STATE'; payload: Partial } - | { type: 'SET_CURRENT_STEP'; payload: string } - | { type: 'COMPLETE_STEP'; payload: string } - | { type: 'SET_CHECKPOINT_STATUS'; payload: { id: string; status: CheckpointStatus } } - | { type: 'ADD_USE_CASE'; payload: UseCaseAssessment } - | { type: 'UPDATE_USE_CASE'; payload: { id: string; data: Partial } } - | { type: 'DELETE_USE_CASE'; payload: string } - | { type: 'SET_ACTIVE_USE_CASE'; payload: string | null } - | { type: 'SET_SCREENING'; payload: ScreeningResult } - | { type: 'ADD_MODULE'; payload: ServiceModule } - | { type: 'UPDATE_MODULE'; payload: { id: string; data: Partial } } - | { type: 'ADD_REQUIREMENT'; payload: Requirement } - | { type: 'UPDATE_REQUIREMENT'; payload: { id: string; data: Partial } } - | { type: 'ADD_CONTROL'; payload: Control } - | { type: 'UPDATE_CONTROL'; payload: { id: string; data: Partial } } - | { type: 'ADD_EVIDENCE'; payload: Evidence } - | { type: 'UPDATE_EVIDENCE'; payload: { id: string; data: Partial } } - | { type: 'DELETE_EVIDENCE'; payload: string } - | { type: 'ADD_RISK'; payload: Risk } - | { type: 'UPDATE_RISK'; payload: { id: string; data: Partial } } - | { type: 'DELETE_RISK'; payload: string } - | { type: 'SET_AI_ACT_RESULT'; payload: AIActResult } - | { type: 'ADD_OBLIGATION'; payload: Obligation } - | { type: 'UPDATE_OBLIGATION'; payload: { id: string; data: Partial } } - | { type: 'SET_DSFA'; payload: DSFA } - | { type: 'ADD_TOM'; payload: TOM } - | { type: 'UPDATE_TOM'; payload: { id: string; data: Partial } } - | { type: 'ADD_RETENTION_POLICY'; payload: RetentionPolicy } - | { type: 'UPDATE_RETENTION_POLICY'; payload: { id: string; data: Partial } } - | { type: 'ADD_PROCESSING_ACTIVITY'; payload: ProcessingActivity } - | { type: 'UPDATE_PROCESSING_ACTIVITY'; payload: { id: string; data: Partial } } - | { type: 'ADD_DOCUMENT'; payload: LegalDocument } - | { type: 'UPDATE_DOCUMENT'; payload: { id: string; data: Partial } } - | { type: 'SET_COOKIE_BANNER'; payload: CookieBannerConfig } - | { type: 'SET_DSR_CONFIG'; payload: DSRConfig } - | { type: 'ADD_DSR_REQUEST'; payload: DSRRequest } - | { type: 'UPDATE_DSR_REQUEST'; payload: { id: string; data: Partial } } - | { type: 'ADD_ESCALATION_WORKFLOW'; payload: EscalationWorkflow } - | { type: 'UPDATE_ESCALATION_WORKFLOW'; payload: { id: string; data: Partial } } - | { type: 'ADD_SECURITY_ISSUE'; payload: SecurityIssue } - | { type: 'UPDATE_SECURITY_ISSUE'; payload: { id: string; data: Partial } } - | { type: 'ADD_BACKLOG_ITEM'; payload: BacklogItem } - | { type: 'UPDATE_BACKLOG_ITEM'; payload: { id: string; data: Partial } } - | { type: 'ADD_COMMAND_HISTORY'; payload: CommandHistory } - | { type: 'SET_PREFERENCES'; payload: Partial } - | { type: 'RESET_STATE' } - -// ============================================================================= -// HELPER FUNCTIONS -// ============================================================================= - -export function getStepById(stepId: string): SDKStep | undefined { - return SDK_STEPS.find(s => s.id === stepId) -} - -export function getStepByUrl(url: string): SDKStep | undefined { - return SDK_STEPS.find(s => s.url === url) -} - -export function getStepsForPhase(phase: SDKPhase): SDKStep[] { - return SDK_STEPS.filter(s => s.phase === phase).sort((a, b) => a.order - b.order) -} - -export function getNextStep(currentStepId: string): SDKStep | undefined { - const currentStep = getStepById(currentStepId) - if (!currentStep) return undefined - - const stepsInPhase = getStepsForPhase(currentStep.phase) - const currentIndex = stepsInPhase.findIndex(s => s.id === currentStepId) - - if (currentIndex < stepsInPhase.length - 1) { - return stepsInPhase[currentIndex + 1] - } - - if (currentStep.phase === 1) { - return getStepsForPhase(2)[0] - } - - return undefined -} - -export function getPreviousStep(currentStepId: string): SDKStep | undefined { - const currentStep = getStepById(currentStepId) - if (!currentStep) return undefined - - const stepsInPhase = getStepsForPhase(currentStep.phase) - const currentIndex = stepsInPhase.findIndex(s => s.id === currentStepId) - - if (currentIndex > 0) { - return stepsInPhase[currentIndex - 1] - } - - if (currentStep.phase === 2) { - const phase1Steps = getStepsForPhase(1) - return phase1Steps[phase1Steps.length - 1] - } - - return undefined -} - -export function getCompletionPercentage(state: SDKState): number { - const totalSteps = SDK_STEPS.length - const completedSteps = state.completedSteps.length - return Math.round((completedSteps / totalSteps) * 100) -} - -export function getPhaseCompletionPercentage(state: SDKState, phase: SDKPhase): number { - const phaseSteps = getStepsForPhase(phase) - const completedPhaseSteps = phaseSteps.filter(s => state.completedSteps.includes(s.id)) - return Math.round((completedPhaseSteps.length / phaseSteps.length) * 100) -} +export { + getCompletionPercentage, + getPhaseCompletionPercentage, + type SDKState, + type SDKAction, +} from './state-core' diff --git a/breakpilot-compliance-sdk/packages/vanilla/src/embed-banner.ts b/breakpilot-compliance-sdk/packages/vanilla/src/embed-banner.ts new file mode 100644 index 0000000..c115c4e --- /dev/null +++ b/breakpilot-compliance-sdk/packages/vanilla/src/embed-banner.ts @@ -0,0 +1,321 @@ +/** + * Cookie banner DOM rendering for the vanilla embed SDK. + * + * Phase 4: extracted from embed.ts. The banner module is pure rendering — + * caller passes in a BannerContext describing current consent state plus + * callbacks for user actions. + */ + +import type { + ConsentPurpose, + CookieBannerPosition, + CookieBannerTheme, +} from '@breakpilot/compliance-sdk-types' +import { + TRANSLATIONS, + createElement, + type BannerLanguage, +} from './embed-translations' + +export interface BannerConfig { + position?: CookieBannerPosition + theme?: CookieBannerTheme + language?: BannerLanguage + privacyPolicyUrl?: string + imprintUrl?: string + texts?: { + title?: string + description?: string + acceptAll?: string + rejectAll?: string + settings?: string + save?: string + } + customColors?: { + background?: string + text?: string + primary?: string + secondary?: string + } +} + +export interface BannerCallbacks { + onAcceptAll: () => void + onRejectAll: () => void + onShowSettings: () => void + onToggleCategory: (category: ConsentPurpose, granted: boolean) => void + onSaveSettings: () => void + onBack: () => void +} + +export interface BannerContext { + config: BannerConfig + consents: Record + callbacks: BannerCallbacks +} + +interface ResolvedColors { + bgColor: string + textColor: string + primaryColor: string + secondaryColor: string + isDark: boolean +} + +function resolveColors(config: BannerConfig): ResolvedColors { + const theme = config.theme || 'LIGHT' + const isDark = theme === 'DARK' + return { + isDark, + bgColor: config.customColors?.background || (isDark ? '#1a1a1a' : '#ffffff'), + textColor: config.customColors?.text || (isDark ? '#ffffff' : '#1a1a1a'), + primaryColor: config.customColors?.primary || (isDark ? '#ffffff' : '#1a1a1a'), + secondaryColor: config.customColors?.secondary || (isDark ? '#ffffff' : '#1a1a1a'), + } +} + +export function createBannerElement(ctx: BannerContext): HTMLElement { + const { config, callbacks } = ctx + const lang: BannerLanguage = config.language || 'de' + const t = TRANSLATIONS[lang] + const position = config.position || 'BOTTOM' + const colors = resolveColors(config) + const { bgColor, textColor, primaryColor, secondaryColor, isDark } = colors + + const container = createElement( + 'div', + { + position: 'fixed', + zIndex: '99999', + left: position === 'CENTER' ? '50%' : '0', + right: position === 'CENTER' ? 'auto' : '0', + top: position === 'TOP' ? '0' : position === 'CENTER' ? '50%' : 'auto', + bottom: position === 'BOTTOM' ? '0' : 'auto', + transform: position === 'CENTER' ? 'translate(-50%, -50%)' : 'none', + maxWidth: position === 'CENTER' ? '500px' : 'none', + backgroundColor: bgColor, + color: textColor, + padding: '20px', + boxShadow: '0 -2px 10px rgba(0, 0, 0, 0.1)', + fontFamily: 'system-ui, -apple-system, sans-serif', + fontSize: '14px', + lineHeight: '1.5', + }, + { id: 'breakpilot-consent-banner', role: 'dialog', 'aria-label': t.title } + ) + + const title = createElement('h3', { + margin: '0 0 10px', + fontSize: '18px', + fontWeight: '600', + }) + title.textContent = config.texts?.title || t.title + + const description = createElement('p', { + margin: '0 0 15px', + opacity: '0.8', + }) + description.textContent = config.texts?.description || t.description + + const buttonsContainer = createElement('div', { + display: 'flex', + flexWrap: 'wrap', + gap: '10px', + alignItems: 'center', + }) + + const acceptBtn = createElement( + 'button', + { + padding: '10px 20px', + borderRadius: '4px', + border: 'none', + cursor: 'pointer', + fontWeight: '500', + backgroundColor: primaryColor, + color: isDark ? '#1a1a1a' : '#ffffff', + }, + { type: 'button' } + ) + acceptBtn.textContent = config.texts?.acceptAll || t.acceptAll + acceptBtn.onclick = () => callbacks.onAcceptAll() + + const rejectBtn = createElement( + 'button', + { + padding: '10px 20px', + borderRadius: '4px', + backgroundColor: 'transparent', + border: `1px solid ${secondaryColor}`, + color: textColor, + cursor: 'pointer', + fontWeight: '500', + }, + { type: 'button' } + ) + rejectBtn.textContent = config.texts?.rejectAll || t.rejectAll + rejectBtn.onclick = () => callbacks.onRejectAll() + + const settingsBtn = createElement( + 'button', + { + padding: '10px 20px', + borderRadius: '4px', + backgroundColor: 'transparent', + border: `1px solid ${secondaryColor}`, + color: textColor, + cursor: 'pointer', + fontWeight: '500', + }, + { type: 'button' } + ) + settingsBtn.textContent = config.texts?.settings || t.settings + settingsBtn.onclick = () => callbacks.onShowSettings() + + const linksContainer = createElement('div', { + marginLeft: 'auto', + fontSize: '12px', + }) + + const privacyLink = createElement('a', { + marginRight: '15px', + color: textColor, + textDecoration: 'none', + }) + privacyLink.href = config.privacyPolicyUrl || '/privacy' + privacyLink.textContent = t.privacy + + const imprintLink = createElement('a', { + color: textColor, + textDecoration: 'none', + }) + imprintLink.href = config.imprintUrl || '/imprint' + imprintLink.textContent = t.imprint + + linksContainer.appendChild(privacyLink) + linksContainer.appendChild(imprintLink) + + buttonsContainer.appendChild(acceptBtn) + buttonsContainer.appendChild(rejectBtn) + buttonsContainer.appendChild(settingsBtn) + buttonsContainer.appendChild(linksContainer) + + container.appendChild(title) + container.appendChild(description) + container.appendChild(buttonsContainer) + + return container +} + +export function renderSettingsPanel(element: HTMLElement, ctx: BannerContext): void { + const { config, consents, callbacks } = ctx + const lang: BannerLanguage = config.language || 'de' + const t = TRANSLATIONS[lang] + const colors = resolveColors(config) + const { textColor, primaryColor, secondaryColor, isDark } = colors + + element.innerHTML = '' + + const title = createElement('h3', { + margin: '0 0 15px', + fontSize: '18px', + fontWeight: '600', + }) + title.textContent = t.settings + + element.appendChild(title) + + const categories: ConsentPurpose[] = [ + 'ESSENTIAL', + 'FUNCTIONAL', + 'ANALYTICS', + 'MARKETING', + ] + + categories.forEach(category => { + const catInfo = t.categories[category] + const row = createElement('div', { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + padding: '10px 0', + borderBottom: `1px solid ${isDark ? '#333' : '#eee'}`, + }) + + const labelContainer = createElement('div', {}) + const labelName = createElement('div', { fontWeight: '500' }) + labelName.textContent = catInfo.name + const labelDesc = createElement('div', { fontSize: '12px', opacity: '0.7' }) + labelDesc.textContent = catInfo.description + + labelContainer.appendChild(labelName) + labelContainer.appendChild(labelDesc) + + const checkbox = createElement( + 'input', + { + width: '20px', + height: '20px', + cursor: category === 'ESSENTIAL' ? 'not-allowed' : 'pointer', + }, + { + type: 'checkbox', + 'data-category': category, + } + ) + checkbox.checked = consents[category] + checkbox.disabled = category === 'ESSENTIAL' + checkbox.onchange = () => { + if (category !== 'ESSENTIAL') { + callbacks.onToggleCategory(category, checkbox.checked) + } + } + + row.appendChild(labelContainer) + row.appendChild(checkbox) + element.appendChild(row) + }) + + const buttonsContainer = createElement('div', { + display: 'flex', + flexWrap: 'wrap', + gap: '10px', + marginTop: '15px', + }) + + const saveBtn = createElement( + 'button', + { + padding: '10px 20px', + borderRadius: '4px', + border: 'none', + cursor: 'pointer', + fontWeight: '500', + backgroundColor: primaryColor, + color: isDark ? '#1a1a1a' : '#ffffff', + }, + { type: 'button' } + ) + saveBtn.textContent = config.texts?.save || t.save + saveBtn.onclick = () => callbacks.onSaveSettings() + + const backBtn = createElement( + 'button', + { + padding: '10px 20px', + borderRadius: '4px', + backgroundColor: 'transparent', + border: `1px solid ${secondaryColor}`, + color: textColor, + cursor: 'pointer', + fontWeight: '500', + }, + { type: 'button' } + ) + backBtn.textContent = 'Zurück' + backBtn.onclick = () => callbacks.onBack() + + buttonsContainer.appendChild(saveBtn) + buttonsContainer.appendChild(backBtn) + element.appendChild(buttonsContainer) +} diff --git a/breakpilot-compliance-sdk/packages/vanilla/src/embed-translations.ts b/breakpilot-compliance-sdk/packages/vanilla/src/embed-translations.ts new file mode 100644 index 0000000..0d01674 --- /dev/null +++ b/breakpilot-compliance-sdk/packages/vanilla/src/embed-translations.ts @@ -0,0 +1,78 @@ +/** + * i18n string catalog for the vanilla embed SDK cookie banner. + * + * Phase 4: extracted from embed.ts. + */ + +import type { ConsentPurpose } from '@breakpilot/compliance-sdk-types' + +export type BannerLanguage = 'de' | 'en' + +export interface CategoryCopy { + name: string + description: string +} + +export interface BannerCopy { + title: string + description: string + acceptAll: string + rejectAll: string + settings: string + save: string + privacy: string + imprint: string + categories: Record +} + +export const TRANSLATIONS: Record = { + de: { + title: 'Cookie-Einwilligung', + description: + 'Wir verwenden Cookies, um Ihre Erfahrung zu verbessern. Weitere Informationen finden Sie in unserer Datenschutzerklärung.', + acceptAll: 'Alle akzeptieren', + rejectAll: 'Nur notwendige', + settings: 'Einstellungen', + save: 'Speichern', + privacy: 'Datenschutz', + imprint: 'Impressum', + categories: { + ESSENTIAL: { name: 'Notwendig', description: 'Erforderlich für die Grundfunktionen' }, + FUNCTIONAL: { name: 'Funktional', description: 'Verbesserte Funktionen' }, + ANALYTICS: { name: 'Analyse', description: 'Nutzungsstatistiken' }, + MARKETING: { name: 'Marketing', description: 'Personalisierte Werbung' }, + PERSONALIZATION: { name: 'Personalisierung', description: 'Angepasste Inhalte' }, + THIRD_PARTY: { name: 'Drittanbieter', description: 'Externe Dienste' }, + }, + }, + en: { + title: 'Cookie Consent', + description: + 'We use cookies to improve your experience. For more information, please see our privacy policy.', + acceptAll: 'Accept All', + rejectAll: 'Reject Non-Essential', + settings: 'Settings', + save: 'Save', + privacy: 'Privacy Policy', + imprint: 'Imprint', + categories: { + ESSENTIAL: { name: 'Essential', description: 'Required for basic functionality' }, + FUNCTIONAL: { name: 'Functional', description: 'Enhanced features' }, + ANALYTICS: { name: 'Analytics', description: 'Usage statistics' }, + MARKETING: { name: 'Marketing', description: 'Personalized advertising' }, + PERSONALIZATION: { name: 'Personalization', description: 'Customized content' }, + THIRD_PARTY: { name: 'Third Party', description: 'External services' }, + }, + }, +} + +export function createElement( + tag: K, + styles: Partial = {}, + attributes: Record = {} +): HTMLElementTagNameMap[K] { + const el = document.createElement(tag) + Object.assign(el.style, styles) + Object.entries(attributes).forEach(([key, value]) => el.setAttribute(key, value)) + return el +} diff --git a/breakpilot-compliance-sdk/packages/vanilla/src/embed.ts b/breakpilot-compliance-sdk/packages/vanilla/src/embed.ts index a62794e..08f10ab 100644 --- a/breakpilot-compliance-sdk/packages/vanilla/src/embed.ts +++ b/breakpilot-compliance-sdk/packages/vanilla/src/embed.ts @@ -16,10 +16,14 @@ import { ComplianceClient } from '@breakpilot/compliance-sdk-core' import type { ConsentPurpose, - CookieBannerPosition, - CookieBannerTheme, DSRRequestType, } from '@breakpilot/compliance-sdk-types' +import { + createBannerElement, + renderSettingsPanel, + type BannerConfig, + type BannerContext, +} from './embed-banner' // ============================================================================ // Types @@ -37,27 +41,7 @@ export interface BreakPilotSDKConfig { debug?: boolean } -export interface BannerConfig { - position?: CookieBannerPosition - theme?: CookieBannerTheme - language?: 'de' | 'en' - privacyPolicyUrl?: string - imprintUrl?: string - texts?: { - title?: string - description?: string - acceptAll?: string - rejectAll?: string - settings?: string - save?: string - } - customColors?: { - background?: string - text?: string - primary?: string - secondary?: string - } -} +export type { BannerConfig } from './embed-banner' // ============================================================================ // Internal State @@ -76,51 +60,6 @@ let _consents: Record = { let _bannerElement: HTMLElement | null = null let _isInitialized = false -// ============================================================================ -// Translations -// ============================================================================ - -const TRANSLATIONS = { - de: { - title: 'Cookie-Einwilligung', - description: - 'Wir verwenden Cookies, um Ihre Erfahrung zu verbessern. Weitere Informationen finden Sie in unserer Datenschutzerklärung.', - acceptAll: 'Alle akzeptieren', - rejectAll: 'Nur notwendige', - settings: 'Einstellungen', - save: 'Speichern', - privacy: 'Datenschutz', - imprint: 'Impressum', - categories: { - ESSENTIAL: { name: 'Notwendig', description: 'Erforderlich für die Grundfunktionen' }, - FUNCTIONAL: { name: 'Funktional', description: 'Verbesserte Funktionen' }, - ANALYTICS: { name: 'Analyse', description: 'Nutzungsstatistiken' }, - MARKETING: { name: 'Marketing', description: 'Personalisierte Werbung' }, - PERSONALIZATION: { name: 'Personalisierung', description: 'Angepasste Inhalte' }, - THIRD_PARTY: { name: 'Drittanbieter', description: 'Externe Dienste' }, - }, - }, - en: { - title: 'Cookie Consent', - description: - 'We use cookies to improve your experience. For more information, please see our privacy policy.', - acceptAll: 'Accept All', - rejectAll: 'Reject Non-Essential', - settings: 'Settings', - save: 'Save', - privacy: 'Privacy Policy', - imprint: 'Imprint', - categories: { - ESSENTIAL: { name: 'Essential', description: 'Required for basic functionality' }, - FUNCTIONAL: { name: 'Functional', description: 'Enhanced features' }, - ANALYTICS: { name: 'Analytics', description: 'Usage statistics' }, - MARKETING: { name: 'Marketing', description: 'Personalized advertising' }, - PERSONALIZATION: { name: 'Personalization', description: 'Customized content' }, - THIRD_PARTY: { name: 'Third Party', description: 'External services' }, - }, - }, -} - // ============================================================================ // Utility Functions // ============================================================================ @@ -149,301 +88,40 @@ function storeConsents(consents: Record): void { } } -function createElement( - tag: K, - styles: Partial = {}, - attributes: Record = {} -): HTMLElementTagNameMap[K] { - const el = document.createElement(tag) - Object.assign(el.style, styles) - Object.entries(attributes).forEach(([key, value]) => el.setAttribute(key, value)) - return el -} - // ============================================================================ -// Banner Implementation +// Banner Orchestration // ============================================================================ -function createBanner(): HTMLElement { - const config = _config! - const bannerConfig = config.bannerConfig || {} - const lang = bannerConfig.language || 'de' - const t = TRANSLATIONS[lang] - const position = bannerConfig.position || 'BOTTOM' - const theme = bannerConfig.theme || 'LIGHT' - - const isDark = theme === 'DARK' - const bgColor = bannerConfig.customColors?.background || (isDark ? '#1a1a1a' : '#ffffff') - const textColor = bannerConfig.customColors?.text || (isDark ? '#ffffff' : '#1a1a1a') - const primaryColor = bannerConfig.customColors?.primary || (isDark ? '#ffffff' : '#1a1a1a') - const secondaryColor = bannerConfig.customColors?.secondary || (isDark ? '#ffffff' : '#1a1a1a') - - // Container - const container = createElement( - 'div', - { - position: 'fixed', - zIndex: '99999', - left: position === 'CENTER' ? '50%' : '0', - right: position === 'CENTER' ? 'auto' : '0', - top: position === 'TOP' ? '0' : position === 'CENTER' ? '50%' : 'auto', - bottom: position === 'BOTTOM' ? '0' : 'auto', - transform: position === 'CENTER' ? 'translate(-50%, -50%)' : 'none', - maxWidth: position === 'CENTER' ? '500px' : 'none', - backgroundColor: bgColor, - color: textColor, - padding: '20px', - boxShadow: '0 -2px 10px rgba(0, 0, 0, 0.1)', - fontFamily: 'system-ui, -apple-system, sans-serif', - fontSize: '14px', - lineHeight: '1.5', - }, - { id: 'breakpilot-consent-banner', role: 'dialog', 'aria-label': t.title } - ) - - // Title - const title = createElement('h3', { - margin: '0 0 10px', - fontSize: '18px', - fontWeight: '600', - }) - title.textContent = bannerConfig.texts?.title || t.title - - // Description - const description = createElement('p', { - margin: '0 0 15px', - opacity: '0.8', - }) - description.textContent = bannerConfig.texts?.description || t.description - - // Buttons container - const buttonsContainer = createElement('div', { - display: 'flex', - flexWrap: 'wrap', - gap: '10px', - alignItems: 'center', - }) - - // Accept All button - const acceptBtn = createElement( - 'button', - { - padding: '10px 20px', - borderRadius: '4px', - border: 'none', - cursor: 'pointer', - fontWeight: '500', - backgroundColor: primaryColor, - color: isDark ? '#1a1a1a' : '#ffffff', - }, - { type: 'button' } - ) - acceptBtn.textContent = bannerConfig.texts?.acceptAll || t.acceptAll - acceptBtn.onclick = () => handleAcceptAll() - - // Reject button - const rejectBtn = createElement( - 'button', - { - padding: '10px 20px', - borderRadius: '4px', - backgroundColor: 'transparent', - border: `1px solid ${secondaryColor}`, - color: textColor, - cursor: 'pointer', - fontWeight: '500', - }, - { type: 'button' } - ) - rejectBtn.textContent = bannerConfig.texts?.rejectAll || t.rejectAll - rejectBtn.onclick = () => handleRejectAll() - - // Settings button - const settingsBtn = createElement( - 'button', - { - padding: '10px 20px', - borderRadius: '4px', - backgroundColor: 'transparent', - border: `1px solid ${secondaryColor}`, - color: textColor, - cursor: 'pointer', - fontWeight: '500', - }, - { type: 'button' } - ) - settingsBtn.textContent = bannerConfig.texts?.settings || t.settings - settingsBtn.onclick = () => showSettingsPanel() - - // Links container - const linksContainer = createElement('div', { - marginLeft: 'auto', - fontSize: '12px', - }) - - const privacyLink = createElement('a', { - marginRight: '15px', - color: textColor, - textDecoration: 'none', - }) - privacyLink.href = bannerConfig.privacyPolicyUrl || '/privacy' - privacyLink.textContent = t.privacy - - const imprintLink = createElement('a', { - color: textColor, - textDecoration: 'none', - }) - imprintLink.href = bannerConfig.imprintUrl || '/imprint' - imprintLink.textContent = t.imprint - - // Assemble - linksContainer.appendChild(privacyLink) - linksContainer.appendChild(imprintLink) - - buttonsContainer.appendChild(acceptBtn) - buttonsContainer.appendChild(rejectBtn) - buttonsContainer.appendChild(settingsBtn) - buttonsContainer.appendChild(linksContainer) - - container.appendChild(title) - container.appendChild(description) - container.appendChild(buttonsContainer) - - return container -} - -function showSettingsPanel(): void { - if (!_bannerElement) return - - const config = _config! - const bannerConfig = config.bannerConfig || {} - const lang = bannerConfig.language || 'de' - const t = TRANSLATIONS[lang] - const theme = bannerConfig.theme || 'LIGHT' - const isDark = theme === 'DARK' - const textColor = bannerConfig.customColors?.text || (isDark ? '#ffffff' : '#1a1a1a') - - // Clear banner content - _bannerElement.innerHTML = '' - - // Title - const title = createElement('h3', { - margin: '0 0 15px', - fontSize: '18px', - fontWeight: '600', - }) - title.textContent = t.settings - - _bannerElement.appendChild(title) - - // Categories - const categories: ConsentPurpose[] = [ - 'ESSENTIAL', - 'FUNCTIONAL', - 'ANALYTICS', - 'MARKETING', - ] - - categories.forEach(category => { - const catInfo = t.categories[category] - const row = createElement('div', { - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - padding: '10px 0', - borderBottom: `1px solid ${isDark ? '#333' : '#eee'}`, - }) - - const labelContainer = createElement('div', {}) - const labelName = createElement('div', { fontWeight: '500' }) - labelName.textContent = catInfo.name - const labelDesc = createElement('div', { fontSize: '12px', opacity: '0.7' }) - labelDesc.textContent = catInfo.description - - labelContainer.appendChild(labelName) - labelContainer.appendChild(labelDesc) - - const checkbox = createElement( - 'input', - { - width: '20px', - height: '20px', - cursor: category === 'ESSENTIAL' ? 'not-allowed' : 'pointer', +function buildBannerContext(): BannerContext { + return { + config: _config?.bannerConfig || {}, + consents: _consents, + callbacks: { + onAcceptAll: () => handleAcceptAll(), + onRejectAll: () => handleRejectAll(), + onShowSettings: () => showSettingsPanel(), + onToggleCategory: (category, granted) => { + _consents[category] = granted }, - { - type: 'checkbox', - 'data-category': category, - } - ) - checkbox.checked = _consents[category] - checkbox.disabled = category === 'ESSENTIAL' - checkbox.onchange = () => { - if (category !== 'ESSENTIAL') { - _consents[category] = checkbox.checked - } - } - - row.appendChild(labelContainer) - row.appendChild(checkbox) - _bannerElement!.appendChild(row) - }) - - // Buttons - const buttonsContainer = createElement('div', { - display: 'flex', - flexWrap: 'wrap', - gap: '10px', - marginTop: '15px', - }) - - const primaryColor = bannerConfig.customColors?.primary || (isDark ? '#ffffff' : '#1a1a1a') - const secondaryColor = bannerConfig.customColors?.secondary || (isDark ? '#ffffff' : '#1a1a1a') - - const saveBtn = createElement( - 'button', - { - padding: '10px 20px', - borderRadius: '4px', - border: 'none', - cursor: 'pointer', - fontWeight: '500', - backgroundColor: primaryColor, - color: isDark ? '#1a1a1a' : '#ffffff', + onSaveSettings: () => handleSaveSettings(), + onBack: () => showMainBanner(), }, - { type: 'button' } - ) - saveBtn.textContent = bannerConfig.texts?.save || t.save - saveBtn.onclick = () => handleSaveSettings() - - const backBtn = createElement( - 'button', - { - padding: '10px 20px', - borderRadius: '4px', - backgroundColor: 'transparent', - border: `1px solid ${secondaryColor}`, - color: textColor, - cursor: 'pointer', - fontWeight: '500', - }, - { type: 'button' } - ) - backBtn.textContent = 'Zurück' - backBtn.onclick = () => showMainBanner() - - buttonsContainer.appendChild(saveBtn) - buttonsContainer.appendChild(backBtn) - _bannerElement.appendChild(buttonsContainer) + } } function showMainBanner(): void { if (_bannerElement) { _bannerElement.remove() } - _bannerElement = createBanner() + _bannerElement = createBannerElement(buildBannerContext()) document.body.appendChild(_bannerElement) } +function showSettingsPanel(): void { + if (!_bannerElement) return + renderSettingsPanel(_bannerElement, buildBannerContext()) +} + function hideBanner(): void { if (_bannerElement) { _bannerElement.remove() @@ -507,7 +185,7 @@ function init(config: BreakPilotSDKConfig): void { _client = new ComplianceClient({ apiEndpoint: config.apiEndpoint, apiKey: config.apiKey, - tenantId: config.tenantId, + tenantId: config.tenantId ?? '', }) // Check for stored consents diff --git a/breakpilot-compliance-sdk/packages/vanilla/src/web-components/base.ts b/breakpilot-compliance-sdk/packages/vanilla/src/web-components/base.ts index 652942d..4e91b18 100644 --- a/breakpilot-compliance-sdk/packages/vanilla/src/web-components/base.ts +++ b/breakpilot-compliance-sdk/packages/vanilla/src/web-components/base.ts @@ -37,7 +37,7 @@ export abstract class BreakPilotElement extends HTMLElement { const apiKey = this.getAttribute('api-key') const apiEndpoint = this.getAttribute('api-endpoint') || 'https://compliance.breakpilot.app/api/v1' - const tenantId = this.getAttribute('tenant-id') || undefined + const tenantId = this.getAttribute('tenant-id') || '' if (apiKey) { this.client = new ComplianceClient({ diff --git a/breakpilot-compliance-sdk/packages/vanilla/src/web-components/consent-banner.ts b/breakpilot-compliance-sdk/packages/vanilla/src/web-components/consent-banner.ts index 1585601..f426c8a 100644 --- a/breakpilot-compliance-sdk/packages/vanilla/src/web-components/consent-banner.ts +++ b/breakpilot-compliance-sdk/packages/vanilla/src/web-components/consent-banner.ts @@ -324,7 +324,7 @@ export class ConsentBannerElement extends BreakPilotElement { private renderSettings(styles: string): void { const t = this.t - const categories: ConsentPurpose[] = ['ESSENTIAL', 'FUNCTIONAL', 'ANALYTICS', 'MARKETING'] + const categories = ['ESSENTIAL', 'FUNCTIONAL', 'ANALYTICS', 'MARKETING'] as const const categoriesHtml = categories .map( diff --git a/breakpilot-compliance-sdk/packages/vue/src/composables/useRAG.ts b/breakpilot-compliance-sdk/packages/vue/src/composables/useRAG.ts index 7d61d4a..8ab8739 100644 --- a/breakpilot-compliance-sdk/packages/vue/src/composables/useRAG.ts +++ b/breakpilot-compliance-sdk/packages/vue/src/composables/useRAG.ts @@ -8,7 +8,7 @@ import type { SearchResponse, AssistantResponse, ChatMessage, - LegalDocument, + RagLegalDocument, } from '@breakpilot/compliance-sdk-types' export interface UseRAGReturn { @@ -23,7 +23,7 @@ export interface UseRAGReturn { isTyping: Ref // Documents - documents: ComputedRef + documents: ComputedRef availableRegulations: ComputedRef // Loading state