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
|
* Compliance Client
|
||||||
*
|
*
|
||||||
* Main entry point for the SDK. Handles API communication with
|
* Main entry point for the SDK. Domain methods delegate to HttpTransport
|
||||||
* retry logic, timeout handling, and optimistic locking.
|
* for retry/timeout/abort handling. Transport primitives live in client-http.ts.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@@ -19,6 +19,11 @@ import type {
|
|||||||
SDKState,
|
SDKState,
|
||||||
CheckpointStatus,
|
CheckpointStatus,
|
||||||
} from '@breakpilot/compliance-sdk-types'
|
} from '@breakpilot/compliance-sdk-types'
|
||||||
|
import {
|
||||||
|
HttpTransport,
|
||||||
|
createHttpError,
|
||||||
|
type APIError,
|
||||||
|
} from './client-http'
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// TYPES
|
// TYPES
|
||||||
@@ -34,157 +39,33 @@ export interface ComplianceClientOptions {
|
|||||||
onAuthError?: () => void
|
onAuthError?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
interface APIError extends Error {
|
|
||||||
status?: number
|
|
||||||
code?: string
|
|
||||||
retryable: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// CONSTANTS
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
const DEFAULT_TIMEOUT = 30000
|
|
||||||
const DEFAULT_MAX_RETRIES = 3
|
|
||||||
const RETRY_DELAYS = [1000, 2000, 4000]
|
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// COMPLIANCE CLIENT
|
// COMPLIANCE CLIENT
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
export class ComplianceClient {
|
export class ComplianceClient {
|
||||||
private apiEndpoint: string
|
private http: HttpTransport
|
||||||
private apiKey: string | null
|
|
||||||
private tenantId: string
|
|
||||||
private timeout: number
|
|
||||||
private maxRetries: number
|
|
||||||
private accessToken: string | null = null
|
|
||||||
private abortControllers: Map<string, AbortController> = new Map()
|
|
||||||
private onError?: (error: Error) => void
|
|
||||||
private onAuthError?: () => void
|
|
||||||
|
|
||||||
constructor(options: ComplianceClientOptions) {
|
constructor(options: ComplianceClientOptions) {
|
||||||
this.apiEndpoint = options.apiEndpoint.replace(/\/$/, '')
|
this.http = new HttpTransport({
|
||||||
this.apiKey = options.apiKey ?? null
|
apiEndpoint: options.apiEndpoint,
|
||||||
this.tenantId = options.tenantId
|
tenantId: options.tenantId,
|
||||||
this.timeout = options.timeout ?? DEFAULT_TIMEOUT
|
timeout: options.timeout,
|
||||||
this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES
|
maxRetries: options.maxRetries,
|
||||||
this.onError = options.onError
|
onError: options.onError,
|
||||||
this.onAuthError = options.onAuthError
|
onAuthError: options.onAuthError,
|
||||||
}
|
})
|
||||||
|
if (options.apiKey) {
|
||||||
// ---------------------------------------------------------------------------
|
this.http.setApiKey(options.apiKey)
|
||||||
// Private Methods
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
private createError(message: string, status?: number, retryable = false): APIError {
|
|
||||||
const error = new Error(message) as APIError
|
|
||||||
error.status = status
|
|
||||||
error.retryable = retryable
|
|
||||||
return error
|
|
||||||
}
|
|
||||||
|
|
||||||
private getHeaders(): Record<string, string> {
|
|
||||||
const headers: Record<string, string> = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Tenant-ID': this.tenantId,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.accessToken) {
|
|
||||||
headers['Authorization'] = `Bearer ${this.accessToken}`
|
|
||||||
} else if (this.apiKey) {
|
|
||||||
headers['Authorization'] = `Bearer ${this.apiKey}`
|
|
||||||
}
|
|
||||||
|
|
||||||
return headers
|
|
||||||
}
|
|
||||||
|
|
||||||
private async fetchWithTimeout(
|
|
||||||
url: string,
|
|
||||||
options: RequestInit,
|
|
||||||
requestId: string
|
|
||||||
): Promise<Response> {
|
|
||||||
const controller = new AbortController()
|
|
||||||
this.abortControllers.set(requestId, controller)
|
|
||||||
|
|
||||||
const timeoutId = setTimeout(() => controller.abort(), this.timeout)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
...options,
|
|
||||||
signal: controller.signal,
|
|
||||||
})
|
|
||||||
return response
|
|
||||||
} finally {
|
|
||||||
clearTimeout(timeoutId)
|
|
||||||
this.abortControllers.delete(requestId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetchWithRetry<T>(
|
private get apiEndpoint(): string {
|
||||||
url: string,
|
return this.http.apiEndpoint
|
||||||
options: RequestInit,
|
|
||||||
retries = this.maxRetries
|
|
||||||
): Promise<T> {
|
|
||||||
const requestId = `${Date.now()}-${Math.random()}`
|
|
||||||
let lastError: Error | null = null
|
|
||||||
|
|
||||||
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
||||||
try {
|
|
||||||
const response = await this.fetchWithTimeout(url, options, requestId)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorBody = await response.text()
|
|
||||||
let errorMessage = `HTTP ${response.status}`
|
|
||||||
|
|
||||||
try {
|
|
||||||
const errorJson = JSON.parse(errorBody)
|
|
||||||
errorMessage = errorJson.error || errorJson.message || errorMessage
|
|
||||||
} catch {
|
|
||||||
// Keep HTTP status message
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle auth errors
|
|
||||||
if (response.status === 401) {
|
|
||||||
this.onAuthError?.()
|
|
||||||
throw this.createError(errorMessage, response.status, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't retry client errors (4xx) except 429
|
|
||||||
const retryable = response.status >= 500 || response.status === 429
|
|
||||||
|
|
||||||
if (!retryable || attempt === retries) {
|
|
||||||
throw this.createError(errorMessage, response.status, retryable)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const data = await response.json()
|
|
||||||
return data as T
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
lastError = error as Error
|
|
||||||
|
|
||||||
if (error instanceof Error && error.name === 'AbortError') {
|
|
||||||
throw this.createError('Request timeout', 408, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiError = error as APIError
|
|
||||||
if (!apiError.retryable || attempt === retries) {
|
|
||||||
this.onError?.(error as Error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exponential backoff
|
|
||||||
if (attempt < retries) {
|
|
||||||
await this.sleep(RETRY_DELAYS[attempt] || RETRY_DELAYS[RETRY_DELAYS.length - 1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw lastError || this.createError('Unknown error', 500, false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private sleep(ms: number): Promise<void> {
|
private get tenantId(): string {
|
||||||
return new Promise(resolve => setTimeout(resolve, ms))
|
return this.http.getTenantId()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -192,7 +73,7 @@ export class ComplianceClient {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async authenticate(request: AuthTokenRequest): Promise<AuthTokenResponse> {
|
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`,
|
`${this.apiEndpoint}/auth/token`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -202,11 +83,11 @@ export class ComplianceClient {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
this.accessToken = response.data.accessToken
|
this.http.setAccessToken(response.data.accessToken)
|
||||||
return response.data
|
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> {
|
async refreshToken(refreshToken: string): Promise<AuthTokenResponse> {
|
||||||
@@ -218,11 +99,11 @@ export class ComplianceClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setAccessToken(token: string): void {
|
setAccessToken(token: string): void {
|
||||||
this.accessToken = token
|
this.http.setAccessToken(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
clearAccessToken(): void {
|
clearAccessToken(): void {
|
||||||
this.accessToken = null
|
this.http.setAccessToken(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -231,11 +112,11 @@ export class ComplianceClient {
|
|||||||
|
|
||||||
async getState(): Promise<StateResponse | null> {
|
async getState(): Promise<StateResponse | null> {
|
||||||
try {
|
try {
|
||||||
const response = await this.fetchWithRetry<APIResponse<StateResponse>>(
|
const response = await this.http.fetchWithRetry<APIResponse<StateResponse>>(
|
||||||
`${this.apiEndpoint}/state?tenantId=${encodeURIComponent(this.tenantId)}`,
|
`${this.apiEndpoint}/state?tenantId=${encodeURIComponent(this.tenantId)}`,
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: this.getHeaders(),
|
headers: this.http.getHeaders(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -254,12 +135,12 @@ export class ComplianceClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async saveState(state: SDKState, version?: number): Promise<StateResponse> {
|
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`,
|
`${this.apiEndpoint}/state`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
...this.getHeaders(),
|
...this.http.getHeaders(),
|
||||||
...(version !== undefined && { 'If-Match': String(version) }),
|
...(version !== undefined && { 'If-Match': String(version) }),
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -271,18 +152,18 @@ export class ComplianceClient {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (!response.success) {
|
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!
|
return response.data!
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteState(): Promise<void> {
|
async deleteState(): Promise<void> {
|
||||||
await this.fetchWithRetry<APIResponse<void>>(
|
await this.http.fetchWithRetry<APIResponse<void>>(
|
||||||
`${this.apiEndpoint}/state?tenantId=${encodeURIComponent(this.tenantId)}`,
|
`${this.apiEndpoint}/state?tenantId=${encodeURIComponent(this.tenantId)}`,
|
||||||
{
|
{
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: this.getHeaders(),
|
headers: this.http.getHeaders(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -295,34 +176,32 @@ export class ComplianceClient {
|
|||||||
checkpointId: string,
|
checkpointId: string,
|
||||||
data?: unknown
|
data?: unknown
|
||||||
): Promise<CheckpointValidationResult> {
|
): Promise<CheckpointValidationResult> {
|
||||||
const response = await this.fetchWithRetry<APIResponse<CheckpointValidationResult>>(
|
const response = await this.http.fetchWithRetry<
|
||||||
`${this.apiEndpoint}/checkpoints/validate`,
|
APIResponse<CheckpointValidationResult>
|
||||||
{
|
>(`${this.apiEndpoint}/checkpoints/validate`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: this.getHeaders(),
|
headers: this.http.getHeaders(),
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
tenantId: this.tenantId,
|
tenantId: this.tenantId,
|
||||||
checkpointId,
|
checkpointId,
|
||||||
data,
|
data,
|
||||||
}),
|
}),
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
if (!response.success || !response.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
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCheckpoints(): Promise<Record<string, CheckpointStatus>> {
|
async getCheckpoints(): Promise<Record<string, CheckpointStatus>> {
|
||||||
const response = await this.fetchWithRetry<APIResponse<Record<string, CheckpointStatus>>>(
|
const response = await this.http.fetchWithRetry<
|
||||||
`${this.apiEndpoint}/checkpoints?tenantId=${encodeURIComponent(this.tenantId)}`,
|
APIResponse<Record<string, CheckpointStatus>>
|
||||||
{
|
>(`${this.apiEndpoint}/checkpoints?tenantId=${encodeURIComponent(this.tenantId)}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: this.getHeaders(),
|
headers: this.http.getHeaders(),
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
return response.data || {}
|
return response.data || {}
|
||||||
}
|
}
|
||||||
@@ -332,34 +211,34 @@ export class ComplianceClient {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async searchRAG(request: RAGSearchRequest): Promise<RAGSearchResponse> {
|
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`,
|
`${this.apiEndpoint}/rag/search`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: this.getHeaders(),
|
headers: this.http.getHeaders(),
|
||||||
body: JSON.stringify(request),
|
body: JSON.stringify(request),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!response.success || !response.data) {
|
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
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
async askRAG(request: RAGAskRequest): Promise<RAGAskResponse> {
|
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`,
|
`${this.apiEndpoint}/rag/ask`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: this.getHeaders(),
|
headers: this.http.getHeaders(),
|
||||||
body: JSON.stringify(request),
|
body: JSON.stringify(request),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!response.success || !response.data) {
|
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
|
return response.data
|
||||||
@@ -370,12 +249,12 @@ export class ComplianceClient {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async exportState(format: ExportFormat): Promise<Blob> {
|
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}`,
|
`${this.apiEndpoint}/export?tenantId=${encodeURIComponent(this.tenantId)}&format=${format}`,
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
...this.getHeaders(),
|
...this.http.getHeaders(),
|
||||||
Accept:
|
Accept:
|
||||||
format === 'json'
|
format === 'json'
|
||||||
? 'application/json'
|
? 'application/json'
|
||||||
@@ -388,7 +267,7 @@ export class ComplianceClient {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (!response.ok) {
|
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()
|
return response.blob()
|
||||||
@@ -402,22 +281,19 @@ export class ComplianceClient {
|
|||||||
type: 'dsfa' | 'tom' | 'vvt' | 'gutachten' | 'privacy_policy' | 'cookie_banner',
|
type: 'dsfa' | 'tom' | 'vvt' | 'gutachten' | 'privacy_policy' | 'cookie_banner',
|
||||||
options?: Record<string, unknown>
|
options?: Record<string, unknown>
|
||||||
): Promise<{ id: string; status: string; content?: string }> {
|
): 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 }>
|
APIResponse<{ id: string; status: string; content?: string }>
|
||||||
>(
|
>(`${this.apiEndpoint}/generate/${type}`, {
|
||||||
`${this.apiEndpoint}/generate/${type}`,
|
method: 'POST',
|
||||||
{
|
headers: this.http.getHeaders(),
|
||||||
method: 'POST',
|
body: JSON.stringify({
|
||||||
headers: this.getHeaders(),
|
tenantId: this.tenantId,
|
||||||
body: JSON.stringify({
|
options,
|
||||||
tenantId: this.tenantId,
|
}),
|
||||||
options,
|
})
|
||||||
}),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!response.success || !response.data) {
|
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
|
return response.data
|
||||||
@@ -433,31 +309,30 @@ export class ComplianceClient {
|
|||||||
severityThreshold?: string
|
severityThreshold?: string
|
||||||
generateSBOM?: boolean
|
generateSBOM?: boolean
|
||||||
}): Promise<{ id: string; status: string }> {
|
}): Promise<{ id: string; status: string }> {
|
||||||
const response = await this.fetchWithRetry<APIResponse<{ id: string; status: string }>>(
|
const response = await this.http.fetchWithRetry<
|
||||||
`${this.apiEndpoint}/security/scan`,
|
APIResponse<{ id: string; status: string }>
|
||||||
{
|
>(`${this.apiEndpoint}/security/scan`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: this.getHeaders(),
|
headers: this.http.getHeaders(),
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
tenantId: this.tenantId,
|
tenantId: this.tenantId,
|
||||||
...options,
|
...options,
|
||||||
}),
|
}),
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
if (!response.success || !response.data) {
|
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
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSecurityScanResult(scanId: string): Promise<unknown> {
|
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}`,
|
`${this.apiEndpoint}/security/scan/${scanId}`,
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: this.getHeaders(),
|
headers: this.http.getHeaders(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -469,21 +344,20 @@ export class ComplianceClient {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
cancelAllRequests(): void {
|
cancelAllRequests(): void {
|
||||||
this.abortControllers.forEach(controller => controller.abort())
|
this.http.cancelAllRequests()
|
||||||
this.abortControllers.clear()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setTenantId(tenantId: string): void {
|
setTenantId(tenantId: string): void {
|
||||||
this.tenantId = tenantId
|
this.http.setTenantId(tenantId)
|
||||||
}
|
}
|
||||||
|
|
||||||
getTenantId(): string {
|
getTenantId(): string {
|
||||||
return this.tenantId
|
return this.http.getTenantId()
|
||||||
}
|
}
|
||||||
|
|
||||||
async healthCheck(): Promise<boolean> {
|
async healthCheck(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const response = await this.fetchWithTimeout(
|
const response = await this.http.fetchWithTimeout(
|
||||||
`${this.apiEndpoint}/health`,
|
`${this.apiEndpoint}/health`,
|
||||||
{ method: 'GET' },
|
{ method: 'GET' },
|
||||||
`health-${Date.now()}`
|
`health-${Date.now()}`
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ import type {
|
|||||||
SearchResponse,
|
SearchResponse,
|
||||||
AssistantQuery,
|
AssistantQuery,
|
||||||
AssistantResponse,
|
AssistantResponse,
|
||||||
LegalDocument,
|
|
||||||
ChatSession,
|
|
||||||
ChatMessage,
|
ChatMessage,
|
||||||
} from '@breakpilot/compliance-sdk-types'
|
} from '@breakpilot/compliance-sdk-types'
|
||||||
import { ComplianceClient } from '../client'
|
import { ComplianceClient } from '../client'
|
||||||
@@ -38,12 +36,20 @@ export class RAGModule {
|
|||||||
scoreThreshold: options?.scoreThreshold ?? 0.5,
|
scoreThreshold: options?.scoreThreshold ?? 0.5,
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.client.searchRAG({
|
const response = await this.client.searchRAG({
|
||||||
query: searchRequest.query,
|
query: searchRequest.query,
|
||||||
filters: searchRequest.filters,
|
filters: searchRequest.filters,
|
||||||
limit: searchRequest.limit,
|
limit: searchRequest.limit,
|
||||||
offset: searchRequest.offset,
|
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> {
|
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'
|
'use client'
|
||||||
|
|
||||||
import React, {
|
import React, {
|
||||||
createContext,
|
|
||||||
useContext,
|
|
||||||
useReducer,
|
useReducer,
|
||||||
useEffect,
|
useEffect,
|
||||||
useCallback,
|
useCallback,
|
||||||
@@ -20,21 +18,13 @@ import {
|
|||||||
createComplianceModule,
|
createComplianceModule,
|
||||||
createRAGModule,
|
createRAGModule,
|
||||||
createSecurityModule,
|
createSecurityModule,
|
||||||
type DSGVOModule,
|
|
||||||
type ComplianceModule,
|
|
||||||
type RAGModule,
|
|
||||||
type SecurityModule,
|
|
||||||
} from '@breakpilot/compliance-sdk-core'
|
} from '@breakpilot/compliance-sdk-core'
|
||||||
import type {
|
import type {
|
||||||
SDKState,
|
|
||||||
SDKAction,
|
|
||||||
SDKStep,
|
|
||||||
CheckpointStatus,
|
CheckpointStatus,
|
||||||
SyncState,
|
SyncState,
|
||||||
UseCaseAssessment,
|
UseCaseAssessment,
|
||||||
Risk,
|
Risk,
|
||||||
Control,
|
Control,
|
||||||
UserPreferences,
|
|
||||||
} from '@breakpilot/compliance-sdk-types'
|
} from '@breakpilot/compliance-sdk-types'
|
||||||
import {
|
import {
|
||||||
getStepById,
|
getStepById,
|
||||||
@@ -43,94 +33,27 @@ import {
|
|||||||
getCompletionPercentage,
|
getCompletionPercentage,
|
||||||
getPhaseCompletionPercentage,
|
getPhaseCompletionPercentage,
|
||||||
} from '@breakpilot/compliance-sdk-types'
|
} from '@breakpilot/compliance-sdk-types'
|
||||||
|
import {
|
||||||
|
ComplianceContext,
|
||||||
|
SDK_STORAGE_KEY,
|
||||||
|
type ComplianceContextValue,
|
||||||
|
type ComplianceProviderProps,
|
||||||
|
} from './provider-context'
|
||||||
|
|
||||||
// =============================================================================
|
export {
|
||||||
// CONTEXT TYPES
|
ComplianceContext,
|
||||||
// =============================================================================
|
type ComplianceContextValue,
|
||||||
|
type ComplianceProviderProps,
|
||||||
|
} from './provider-context'
|
||||||
|
|
||||||
export interface ComplianceContextValue {
|
// Re-export useCompliance so legacy component imports (`from '../provider'`)
|
||||||
// State
|
// keep resolving. Pre-existing cross-file import that was broken in baseline.
|
||||||
state: SDKState
|
export { useCompliance } from './hooks'
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// PROVIDER
|
// PROVIDER
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
const SDK_STORAGE_KEY = 'breakpilot-compliance-sdk-state'
|
|
||||||
|
|
||||||
export function ComplianceProvider({
|
export function ComplianceProvider({
|
||||||
children,
|
children,
|
||||||
apiEndpoint,
|
apiEndpoint,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import type { RegulationCode } from './compliance'
|
|||||||
// LEGAL CORPUS
|
// LEGAL CORPUS
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
export interface LegalDocument {
|
export interface RagLegalDocument {
|
||||||
id: string
|
id: string
|
||||||
code: RegulationCode | string
|
code: RegulationCode | string
|
||||||
name: 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 {
|
export {
|
||||||
SubscriptionTier,
|
SDK_STEPS,
|
||||||
UserPreferences,
|
getStepById,
|
||||||
Risk,
|
getStepByUrl,
|
||||||
CommandHistory,
|
getStepsForPhase,
|
||||||
CheckpointStatus,
|
getNextStep,
|
||||||
} from './base'
|
getPreviousStep,
|
||||||
import type {
|
type SDKPhase,
|
||||||
ConsentRecord,
|
type SDKStep,
|
||||||
DSRConfig,
|
} from './state-flow'
|
||||||
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 type {
|
||||||
// SDK FLOW & NAVIGATION
|
UseCaseStep,
|
||||||
// =============================================================================
|
AssessmentResult,
|
||||||
|
UseCaseAssessment,
|
||||||
|
ScreeningResult,
|
||||||
|
} from './state-assessment'
|
||||||
|
|
||||||
export type SDKPhase = 1 | 2
|
export {
|
||||||
|
getCompletionPercentage,
|
||||||
export interface SDKStep {
|
getPhaseCompletionPercentage,
|
||||||
id: string
|
type SDKState,
|
||||||
phase: SDKPhase
|
type SDKAction,
|
||||||
order: number
|
} from './state-core'
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|||||||
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 { ComplianceClient } from '@breakpilot/compliance-sdk-core'
|
||||||
import type {
|
import type {
|
||||||
ConsentPurpose,
|
ConsentPurpose,
|
||||||
CookieBannerPosition,
|
|
||||||
CookieBannerTheme,
|
|
||||||
DSRRequestType,
|
DSRRequestType,
|
||||||
} from '@breakpilot/compliance-sdk-types'
|
} from '@breakpilot/compliance-sdk-types'
|
||||||
|
import {
|
||||||
|
createBannerElement,
|
||||||
|
renderSettingsPanel,
|
||||||
|
type BannerConfig,
|
||||||
|
type BannerContext,
|
||||||
|
} from './embed-banner'
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Types
|
// Types
|
||||||
@@ -37,27 +41,7 @@ export interface BreakPilotSDKConfig {
|
|||||||
debug?: boolean
|
debug?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BannerConfig {
|
export type { BannerConfig } from './embed-banner'
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Internal State
|
// Internal State
|
||||||
@@ -76,51 +60,6 @@ let _consents: Record<ConsentPurpose, boolean> = {
|
|||||||
let _bannerElement: HTMLElement | null = null
|
let _bannerElement: HTMLElement | null = null
|
||||||
let _isInitialized = false
|
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
|
// 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 {
|
function buildBannerContext(): BannerContext {
|
||||||
const config = _config!
|
return {
|
||||||
const bannerConfig = config.bannerConfig || {}
|
config: _config?.bannerConfig || {},
|
||||||
const lang = bannerConfig.language || 'de'
|
consents: _consents,
|
||||||
const t = TRANSLATIONS[lang]
|
callbacks: {
|
||||||
const position = bannerConfig.position || 'BOTTOM'
|
onAcceptAll: () => handleAcceptAll(),
|
||||||
const theme = bannerConfig.theme || 'LIGHT'
|
onRejectAll: () => handleRejectAll(),
|
||||||
|
onShowSettings: () => showSettingsPanel(),
|
||||||
const isDark = theme === 'DARK'
|
onToggleCategory: (category, granted) => {
|
||||||
const bgColor = bannerConfig.customColors?.background || (isDark ? '#1a1a1a' : '#ffffff')
|
_consents[category] = granted
|
||||||
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',
|
|
||||||
},
|
},
|
||||||
{
|
onSaveSettings: () => handleSaveSettings(),
|
||||||
type: 'checkbox',
|
onBack: () => showMainBanner(),
|
||||||
'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',
|
|
||||||
},
|
},
|
||||||
{ 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 {
|
function showMainBanner(): void {
|
||||||
if (_bannerElement) {
|
if (_bannerElement) {
|
||||||
_bannerElement.remove()
|
_bannerElement.remove()
|
||||||
}
|
}
|
||||||
_bannerElement = createBanner()
|
_bannerElement = createBannerElement(buildBannerContext())
|
||||||
document.body.appendChild(_bannerElement)
|
document.body.appendChild(_bannerElement)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showSettingsPanel(): void {
|
||||||
|
if (!_bannerElement) return
|
||||||
|
renderSettingsPanel(_bannerElement, buildBannerContext())
|
||||||
|
}
|
||||||
|
|
||||||
function hideBanner(): void {
|
function hideBanner(): void {
|
||||||
if (_bannerElement) {
|
if (_bannerElement) {
|
||||||
_bannerElement.remove()
|
_bannerElement.remove()
|
||||||
@@ -507,7 +185,7 @@ function init(config: BreakPilotSDKConfig): void {
|
|||||||
_client = new ComplianceClient({
|
_client = new ComplianceClient({
|
||||||
apiEndpoint: config.apiEndpoint,
|
apiEndpoint: config.apiEndpoint,
|
||||||
apiKey: config.apiKey,
|
apiKey: config.apiKey,
|
||||||
tenantId: config.tenantId,
|
tenantId: config.tenantId ?? '',
|
||||||
})
|
})
|
||||||
|
|
||||||
// Check for stored consents
|
// Check for stored consents
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export abstract class BreakPilotElement extends HTMLElement {
|
|||||||
const apiKey = this.getAttribute('api-key')
|
const apiKey = this.getAttribute('api-key')
|
||||||
const apiEndpoint =
|
const apiEndpoint =
|
||||||
this.getAttribute('api-endpoint') || 'https://compliance.breakpilot.app/api/v1'
|
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) {
|
if (apiKey) {
|
||||||
this.client = new ComplianceClient({
|
this.client = new ComplianceClient({
|
||||||
|
|||||||
@@ -324,7 +324,7 @@ export class ConsentBannerElement extends BreakPilotElement {
|
|||||||
|
|
||||||
private renderSettings(styles: string): void {
|
private renderSettings(styles: string): void {
|
||||||
const t = this.t
|
const t = this.t
|
||||||
const categories: ConsentPurpose[] = ['ESSENTIAL', 'FUNCTIONAL', 'ANALYTICS', 'MARKETING']
|
const categories = ['ESSENTIAL', 'FUNCTIONAL', 'ANALYTICS', 'MARKETING'] as const
|
||||||
|
|
||||||
const categoriesHtml = categories
|
const categoriesHtml = categories
|
||||||
.map(
|
.map(
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type {
|
|||||||
SearchResponse,
|
SearchResponse,
|
||||||
AssistantResponse,
|
AssistantResponse,
|
||||||
ChatMessage,
|
ChatMessage,
|
||||||
LegalDocument,
|
RagLegalDocument,
|
||||||
} from '@breakpilot/compliance-sdk-types'
|
} from '@breakpilot/compliance-sdk-types'
|
||||||
|
|
||||||
export interface UseRAGReturn {
|
export interface UseRAGReturn {
|
||||||
@@ -23,7 +23,7 @@ export interface UseRAGReturn {
|
|||||||
isTyping: Ref<boolean>
|
isTyping: Ref<boolean>
|
||||||
|
|
||||||
// Documents
|
// Documents
|
||||||
documents: ComputedRef<LegalDocument[]>
|
documents: ComputedRef<RagLegalDocument[]>
|
||||||
availableRegulations: ComputedRef<string[]>
|
availableRegulations: ComputedRef<string[]>
|
||||||
|
|
||||||
// Loading state
|
// Loading state
|
||||||
|
|||||||
Reference in New Issue
Block a user