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:
187
breakpilot-compliance-sdk/packages/core/src/client-http.ts
Normal file
187
breakpilot-compliance-sdk/packages/core/src/client-http.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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()}`
|
||||
|
||||
@@ -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> {
|
||||
|
||||
101
breakpilot-compliance-sdk/packages/react/src/provider-context.ts
Normal file
101
breakpilot-compliance-sdk/packages/react/src/provider-context.ts
Normal 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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -14,7 +14,7 @@ import type { RegulationCode } from './compliance'
|
||||
// LEGAL CORPUS
|
||||
// =============================================================================
|
||||
|
||||
export interface LegalDocument {
|
||||
export interface RagLegalDocument {
|
||||
id: string
|
||||
code: RegulationCode | string
|
||||
name: string
|
||||
|
||||
@@ -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
|
||||
}
|
||||
161
breakpilot-compliance-sdk/packages/types/src/state-core.ts
Normal file
161
breakpilot-compliance-sdk/packages/types/src/state-core.ts
Normal 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)
|
||||
}
|
||||
311
breakpilot-compliance-sdk/packages/types/src/state-flow.ts
Normal file
311
breakpilot-compliance-sdk/packages/types/src/state-flow.ts
Normal 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
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
321
breakpilot-compliance-sdk/packages/vanilla/src/embed-banner.ts
Normal file
321
breakpilot-compliance-sdk/packages/vanilla/src/embed-banner.ts
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user