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) <noreply@anthropic.com>
This commit is contained in:
Sharang Parnerkar
2026-04-11 22:39:47 +02:00
parent 4ed39d2616
commit 5cb91e88d2
16 changed files with 1380 additions and 1162 deletions

View File

@@ -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<void> =>
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<string, AbortController> = 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<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
}
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)
}
}
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 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()
}
}

View File

@@ -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<string, AbortController> = 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<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)
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<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 get apiEndpoint(): string {
return this.http.apiEndpoint
}
private sleep(ms: number): Promise<void> {
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<AuthTokenResponse> {
const response = await this.fetchWithRetry<APIResponse<AuthTokenResponse>>(
const response = await this.http.fetchWithRetry<APIResponse<AuthTokenResponse>>(
`${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<AuthTokenResponse> {
@@ -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<StateResponse | null> {
try {
const response = await this.fetchWithRetry<APIResponse<StateResponse>>(
const response = await this.http.fetchWithRetry<APIResponse<StateResponse>>(
`${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<StateResponse> {
const response = await this.fetchWithRetry<APIResponse<StateResponse>>(
const response = await this.http.fetchWithRetry<APIResponse<StateResponse>>(
`${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<void> {
await this.fetchWithRetry<APIResponse<void>>(
await this.http.fetchWithRetry<APIResponse<void>>(
`${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<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,
}),
}
)
const response = await this.http.fetchWithRetry<
APIResponse<CheckpointValidationResult>
>(`${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<Record<string, CheckpointStatus>> {
const response = await this.fetchWithRetry<APIResponse<Record<string, CheckpointStatus>>>(
`${this.apiEndpoint}/checkpoints?tenantId=${encodeURIComponent(this.tenantId)}`,
{
method: 'GET',
headers: this.getHeaders(),
}
)
const response = await this.http.fetchWithRetry<
APIResponse<Record<string, CheckpointStatus>>
>(`${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<RAGSearchResponse> {
const response = await this.fetchWithRetry<APIResponse<RAGSearchResponse>>(
const response = await this.http.fetchWithRetry<APIResponse<RAGSearchResponse>>(
`${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<RAGAskResponse> {
const response = await this.fetchWithRetry<APIResponse<RAGAskResponse>>(
const response = await this.http.fetchWithRetry<APIResponse<RAGAskResponse>>(
`${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<Blob> {
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<string, unknown>
): 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<APIResponse<{ id: string; status: string }>>(
`${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<unknown> {
const response = await this.fetchWithRetry<APIResponse<unknown>>(
const response = await this.http.fetchWithRetry<APIResponse<unknown>>(
`${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<boolean> {
try {
const response = await this.fetchWithTimeout(
const response = await this.http.fetchWithTimeout(
`${this.apiEndpoint}/health`,
{ method: 'GET' },
`health-${Date.now()}`

View File

@@ -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<SearchResponse> {

View File

@@ -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<SDKAction>
// 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<CheckpointStatus>
overrideCheckpoint: (checkpointId: string, reason: string) => Promise<void>
getCheckpointStatus: (checkpointId: string) => CheckpointStatus | undefined
// State Updates
updateUseCase: (id: string, data: Partial<UseCaseAssessment>) => void
addRisk: (risk: Risk) => void
updateControl: (id: string, data: Partial<Control>) => void
// Persistence
saveState: () => Promise<void>
loadState: () => Promise<void>
resetState: () => void
// Sync
syncState: SyncState
forceSyncToServer: () => Promise<void>
isOnline: boolean
// Export
exportState: (format: 'json' | 'pdf' | 'zip') => Promise<Blob>
// Command Bar
isCommandBarOpen: boolean
setCommandBarOpen: (open: boolean) => void
// Status
isInitialized: boolean
isLoading: boolean
error: Error | null
}
export const ComplianceContext = createContext<ComplianceContextValue | null>(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
}

View File

@@ -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<SDKAction>
// 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<CheckpointStatus>
overrideCheckpoint: (checkpointId: string, reason: string) => Promise<void>
getCheckpointStatus: (checkpointId: string) => CheckpointStatus | undefined
// State Updates
updateUseCase: (id: string, data: Partial<UseCaseAssessment>) => void
addRisk: (risk: Risk) => void
updateControl: (id: string, data: Partial<Control>) => void
// Persistence
saveState: () => Promise<void>
loadState: () => Promise<void>
resetState: () => void
// Sync
syncState: SyncState
forceSyncToServer: () => Promise<void>
isOnline: boolean
// Export
exportState: (format: 'json' | 'pdf' | 'zip') => Promise<Blob>
// Command Bar
isCommandBarOpen: boolean
setCommandBarOpen: (open: boolean) => void
// Status
isInitialized: boolean
isLoading: boolean
error: Error | null
}
export const ComplianceContext = createContext<ComplianceContextValue | null>(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,

View File

@@ -14,7 +14,7 @@ import type { RegulationCode } from './compliance'
// LEGAL CORPUS
// =============================================================================
export interface LegalDocument {
export interface RagLegalDocument {
id: string
code: RegulationCode | string
name: string

View File

@@ -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<string, unknown>
}
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
}

View File

@@ -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<string, CheckpointStatus>
// 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<SDKState> }
| { 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<UseCaseAssessment> } }
| { 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<ServiceModule> } }
| { type: 'ADD_REQUIREMENT'; payload: Requirement }
| { type: 'UPDATE_REQUIREMENT'; payload: { id: string; data: Partial<Requirement> } }
| { type: 'ADD_CONTROL'; payload: Control }
| { type: 'UPDATE_CONTROL'; payload: { id: string; data: Partial<Control> } }
| { type: 'ADD_EVIDENCE'; payload: Evidence }
| { type: 'UPDATE_EVIDENCE'; payload: { id: string; data: Partial<Evidence> } }
| { type: 'DELETE_EVIDENCE'; payload: string }
| { type: 'ADD_RISK'; payload: Risk }
| { type: 'UPDATE_RISK'; payload: { id: string; data: Partial<Risk> } }
| { 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<Obligation> } }
| { type: 'SET_DSFA'; payload: DSFA }
| { type: 'ADD_TOM'; payload: TOM }
| { type: 'UPDATE_TOM'; payload: { id: string; data: Partial<TOM> } }
| { type: 'ADD_RETENTION_POLICY'; payload: RetentionPolicy }
| { type: 'UPDATE_RETENTION_POLICY'; payload: { id: string; data: Partial<RetentionPolicy> } }
| { type: 'ADD_PROCESSING_ACTIVITY'; payload: ProcessingActivity }
| { type: 'UPDATE_PROCESSING_ACTIVITY'; payload: { id: string; data: Partial<ProcessingActivity> } }
| { type: 'ADD_DOCUMENT'; payload: LegalDocument }
| { type: 'UPDATE_DOCUMENT'; payload: { id: string; data: Partial<LegalDocument> } }
| { 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<DSRRequest> } }
| { type: 'ADD_ESCALATION_WORKFLOW'; payload: EscalationWorkflow }
| { type: 'UPDATE_ESCALATION_WORKFLOW'; payload: { id: string; data: Partial<EscalationWorkflow> } }
| { type: 'ADD_SECURITY_ISSUE'; payload: SecurityIssue }
| { type: 'UPDATE_SECURITY_ISSUE'; payload: { id: string; data: Partial<SecurityIssue> } }
| { type: 'ADD_BACKLOG_ITEM'; payload: BacklogItem }
| { type: 'UPDATE_BACKLOG_ITEM'; payload: { id: string; data: Partial<BacklogItem> } }
| { type: 'ADD_COMMAND_HISTORY'; payload: CommandHistory }
| { type: 'SET_PREFERENCES'; payload: Partial<UserPreferences> }
| { 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)
}

View File

@@ -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
}

View File

@@ -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<string, unknown>
}
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<string, CheckpointStatus>
// 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<SDKState> }
| { 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<UseCaseAssessment> } }
| { 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<ServiceModule> } }
| { type: 'ADD_REQUIREMENT'; payload: Requirement }
| { type: 'UPDATE_REQUIREMENT'; payload: { id: string; data: Partial<Requirement> } }
| { type: 'ADD_CONTROL'; payload: Control }
| { type: 'UPDATE_CONTROL'; payload: { id: string; data: Partial<Control> } }
| { type: 'ADD_EVIDENCE'; payload: Evidence }
| { type: 'UPDATE_EVIDENCE'; payload: { id: string; data: Partial<Evidence> } }
| { type: 'DELETE_EVIDENCE'; payload: string }
| { type: 'ADD_RISK'; payload: Risk }
| { type: 'UPDATE_RISK'; payload: { id: string; data: Partial<Risk> } }
| { 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<Obligation> } }
| { type: 'SET_DSFA'; payload: DSFA }
| { type: 'ADD_TOM'; payload: TOM }
| { type: 'UPDATE_TOM'; payload: { id: string; data: Partial<TOM> } }
| { type: 'ADD_RETENTION_POLICY'; payload: RetentionPolicy }
| { type: 'UPDATE_RETENTION_POLICY'; payload: { id: string; data: Partial<RetentionPolicy> } }
| { type: 'ADD_PROCESSING_ACTIVITY'; payload: ProcessingActivity }
| { type: 'UPDATE_PROCESSING_ACTIVITY'; payload: { id: string; data: Partial<ProcessingActivity> } }
| { type: 'ADD_DOCUMENT'; payload: LegalDocument }
| { type: 'UPDATE_DOCUMENT'; payload: { id: string; data: Partial<LegalDocument> } }
| { 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<DSRRequest> } }
| { type: 'ADD_ESCALATION_WORKFLOW'; payload: EscalationWorkflow }
| { type: 'UPDATE_ESCALATION_WORKFLOW'; payload: { id: string; data: Partial<EscalationWorkflow> } }
| { type: 'ADD_SECURITY_ISSUE'; payload: SecurityIssue }
| { type: 'UPDATE_SECURITY_ISSUE'; payload: { id: string; data: Partial<SecurityIssue> } }
| { type: 'ADD_BACKLOG_ITEM'; payload: BacklogItem }
| { type: 'UPDATE_BACKLOG_ITEM'; payload: { id: string; data: Partial<BacklogItem> } }
| { type: 'ADD_COMMAND_HISTORY'; payload: CommandHistory }
| { type: 'SET_PREFERENCES'; payload: Partial<UserPreferences> }
| { 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'

View File

@@ -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<ConsentPurpose, boolean>
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)
}

View File

@@ -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<ConsentPurpose, CategoryCopy>
}
export const TRANSLATIONS: Record<BannerLanguage, BannerCopy> = {
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<K extends keyof HTMLElementTagNameMap>(
tag: K,
styles: Partial<CSSStyleDeclaration> = {},
attributes: Record<string, string> = {}
): HTMLElementTagNameMap[K] {
const el = document.createElement(tag)
Object.assign(el.style, styles)
Object.entries(attributes).forEach(([key, value]) => el.setAttribute(key, value))
return el
}

View File

@@ -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<ConsentPurpose, boolean> = {
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<ConsentPurpose, boolean>): void {
}
}
function createElement<K extends keyof HTMLElementTagNameMap>(
tag: K,
styles: Partial<CSSStyleDeclaration> = {},
attributes: Record<string, string> = {}
): 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

View File

@@ -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({

View File

@@ -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(

View File

@@ -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<boolean>
// Documents
documents: ComputedRef<LegalDocument[]>
documents: ComputedRef<RagLegalDocument[]>
availableRegulations: ComputedRef<string[]>
// Loading state