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>
396 lines
11 KiB
TypeScript
396 lines
11 KiB
TypeScript
/**
|
|
* Compliance Client
|
|
*
|
|
* Main entry point for the SDK. Domain methods delegate to HttpTransport
|
|
* for retry/timeout/abort handling. Transport primitives live in client-http.ts.
|
|
*/
|
|
|
|
import type {
|
|
APIResponse,
|
|
StateResponse,
|
|
CheckpointValidationResult,
|
|
AuthTokenRequest,
|
|
AuthTokenResponse,
|
|
RAGSearchRequest,
|
|
RAGSearchResponse,
|
|
RAGAskRequest,
|
|
RAGAskResponse,
|
|
ExportFormat,
|
|
SDKState,
|
|
CheckpointStatus,
|
|
} from '@breakpilot/compliance-sdk-types'
|
|
import {
|
|
HttpTransport,
|
|
createHttpError,
|
|
type APIError,
|
|
} from './client-http'
|
|
|
|
// =============================================================================
|
|
// TYPES
|
|
// =============================================================================
|
|
|
|
export interface ComplianceClientOptions {
|
|
apiEndpoint: string
|
|
apiKey?: string
|
|
tenantId: string
|
|
timeout?: number
|
|
maxRetries?: number
|
|
onError?: (error: Error) => void
|
|
onAuthError?: () => void
|
|
}
|
|
|
|
// =============================================================================
|
|
// COMPLIANCE CLIENT
|
|
// =============================================================================
|
|
|
|
export class ComplianceClient {
|
|
private http: HttpTransport
|
|
|
|
constructor(options: ComplianceClientOptions) {
|
|
this.http = new HttpTransport({
|
|
apiEndpoint: options.apiEndpoint,
|
|
tenantId: options.tenantId,
|
|
timeout: options.timeout,
|
|
maxRetries: options.maxRetries,
|
|
onError: options.onError,
|
|
onAuthError: options.onAuthError,
|
|
})
|
|
if (options.apiKey) {
|
|
this.http.setApiKey(options.apiKey)
|
|
}
|
|
}
|
|
|
|
private get apiEndpoint(): string {
|
|
return this.http.apiEndpoint
|
|
}
|
|
|
|
private get tenantId(): string {
|
|
return this.http.getTenantId()
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Authentication
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async authenticate(request: AuthTokenRequest): Promise<AuthTokenResponse> {
|
|
const response = await this.http.fetchWithRetry<APIResponse<AuthTokenResponse>>(
|
|
`${this.apiEndpoint}/auth/token`,
|
|
{
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(request),
|
|
}
|
|
)
|
|
|
|
if (response.success && response.data) {
|
|
this.http.setAccessToken(response.data.accessToken)
|
|
return response.data
|
|
}
|
|
|
|
throw createHttpError(response.error || 'Authentication failed', 401, false)
|
|
}
|
|
|
|
async refreshToken(refreshToken: string): Promise<AuthTokenResponse> {
|
|
return this.authenticate({
|
|
grantType: 'refresh_token',
|
|
clientId: '',
|
|
refreshToken,
|
|
})
|
|
}
|
|
|
|
setAccessToken(token: string): void {
|
|
this.http.setAccessToken(token)
|
|
}
|
|
|
|
clearAccessToken(): void {
|
|
this.http.setAccessToken(null)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// State Management
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async getState(): Promise<StateResponse | null> {
|
|
try {
|
|
const response = await this.http.fetchWithRetry<APIResponse<StateResponse>>(
|
|
`${this.apiEndpoint}/state?tenantId=${encodeURIComponent(this.tenantId)}`,
|
|
{
|
|
method: 'GET',
|
|
headers: this.http.getHeaders(),
|
|
}
|
|
)
|
|
|
|
if (response.success && response.data) {
|
|
return response.data
|
|
}
|
|
|
|
return null
|
|
} catch (error) {
|
|
const apiError = error as APIError
|
|
if (apiError.status === 404) {
|
|
return null
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
|
|
async saveState(state: SDKState, version?: number): Promise<StateResponse> {
|
|
const response = await this.http.fetchWithRetry<APIResponse<StateResponse>>(
|
|
`${this.apiEndpoint}/state`,
|
|
{
|
|
method: 'POST',
|
|
headers: {
|
|
...this.http.getHeaders(),
|
|
...(version !== undefined && { 'If-Match': String(version) }),
|
|
},
|
|
body: JSON.stringify({
|
|
tenantId: this.tenantId,
|
|
state,
|
|
version,
|
|
}),
|
|
}
|
|
)
|
|
|
|
if (!response.success) {
|
|
throw createHttpError(response.error || 'Failed to save state', 500, true)
|
|
}
|
|
|
|
return response.data!
|
|
}
|
|
|
|
async deleteState(): Promise<void> {
|
|
await this.http.fetchWithRetry<APIResponse<void>>(
|
|
`${this.apiEndpoint}/state?tenantId=${encodeURIComponent(this.tenantId)}`,
|
|
{
|
|
method: 'DELETE',
|
|
headers: this.http.getHeaders(),
|
|
}
|
|
)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Checkpoints
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async validateCheckpoint(
|
|
checkpointId: string,
|
|
data?: unknown
|
|
): Promise<CheckpointValidationResult> {
|
|
const response = await this.http.fetchWithRetry<
|
|
APIResponse<CheckpointValidationResult>
|
|
>(`${this.apiEndpoint}/checkpoints/validate`, {
|
|
method: 'POST',
|
|
headers: this.http.getHeaders(),
|
|
body: JSON.stringify({
|
|
tenantId: this.tenantId,
|
|
checkpointId,
|
|
data,
|
|
}),
|
|
})
|
|
|
|
if (!response.success || !response.data) {
|
|
throw createHttpError(response.error || 'Checkpoint validation failed', 500, true)
|
|
}
|
|
|
|
return response.data
|
|
}
|
|
|
|
async getCheckpoints(): Promise<Record<string, CheckpointStatus>> {
|
|
const response = await this.http.fetchWithRetry<
|
|
APIResponse<Record<string, CheckpointStatus>>
|
|
>(`${this.apiEndpoint}/checkpoints?tenantId=${encodeURIComponent(this.tenantId)}`, {
|
|
method: 'GET',
|
|
headers: this.http.getHeaders(),
|
|
})
|
|
|
|
return response.data || {}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// RAG
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async searchRAG(request: RAGSearchRequest): Promise<RAGSearchResponse> {
|
|
const response = await this.http.fetchWithRetry<APIResponse<RAGSearchResponse>>(
|
|
`${this.apiEndpoint}/rag/search`,
|
|
{
|
|
method: 'POST',
|
|
headers: this.http.getHeaders(),
|
|
body: JSON.stringify(request),
|
|
}
|
|
)
|
|
|
|
if (!response.success || !response.data) {
|
|
throw createHttpError(response.error || 'RAG search failed', 500, true)
|
|
}
|
|
|
|
return response.data
|
|
}
|
|
|
|
async askRAG(request: RAGAskRequest): Promise<RAGAskResponse> {
|
|
const response = await this.http.fetchWithRetry<APIResponse<RAGAskResponse>>(
|
|
`${this.apiEndpoint}/rag/ask`,
|
|
{
|
|
method: 'POST',
|
|
headers: this.http.getHeaders(),
|
|
body: JSON.stringify(request),
|
|
}
|
|
)
|
|
|
|
if (!response.success || !response.data) {
|
|
throw createHttpError(response.error || 'RAG query failed', 500, true)
|
|
}
|
|
|
|
return response.data
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Export
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async exportState(format: ExportFormat): Promise<Blob> {
|
|
const response = await this.http.fetchWithTimeout(
|
|
`${this.apiEndpoint}/export?tenantId=${encodeURIComponent(this.tenantId)}&format=${format}`,
|
|
{
|
|
method: 'GET',
|
|
headers: {
|
|
...this.http.getHeaders(),
|
|
Accept:
|
|
format === 'json'
|
|
? 'application/json'
|
|
: format === 'pdf'
|
|
? 'application/pdf'
|
|
: 'application/octet-stream',
|
|
},
|
|
},
|
|
`export-${Date.now()}`
|
|
)
|
|
|
|
if (!response.ok) {
|
|
throw createHttpError(`Export failed: ${response.statusText}`, response.status, true)
|
|
}
|
|
|
|
return response.blob()
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Document Generation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async generateDocument(
|
|
type: 'dsfa' | 'tom' | 'vvt' | 'gutachten' | 'privacy_policy' | 'cookie_banner',
|
|
options?: Record<string, unknown>
|
|
): Promise<{ id: string; status: string; content?: string }> {
|
|
const response = await this.http.fetchWithRetry<
|
|
APIResponse<{ id: string; status: string; content?: string }>
|
|
>(`${this.apiEndpoint}/generate/${type}`, {
|
|
method: 'POST',
|
|
headers: this.http.getHeaders(),
|
|
body: JSON.stringify({
|
|
tenantId: this.tenantId,
|
|
options,
|
|
}),
|
|
})
|
|
|
|
if (!response.success || !response.data) {
|
|
throw createHttpError(response.error || 'Document generation failed', 500, true)
|
|
}
|
|
|
|
return response.data
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Security Scan
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async startSecurityScan(options?: {
|
|
tools?: string[]
|
|
targetPath?: string
|
|
severityThreshold?: string
|
|
generateSBOM?: boolean
|
|
}): Promise<{ id: string; status: string }> {
|
|
const response = await this.http.fetchWithRetry<
|
|
APIResponse<{ id: string; status: string }>
|
|
>(`${this.apiEndpoint}/security/scan`, {
|
|
method: 'POST',
|
|
headers: this.http.getHeaders(),
|
|
body: JSON.stringify({
|
|
tenantId: this.tenantId,
|
|
...options,
|
|
}),
|
|
})
|
|
|
|
if (!response.success || !response.data) {
|
|
throw createHttpError(response.error || 'Security scan failed', 500, true)
|
|
}
|
|
|
|
return response.data
|
|
}
|
|
|
|
async getSecurityScanResult(scanId: string): Promise<unknown> {
|
|
const response = await this.http.fetchWithRetry<APIResponse<unknown>>(
|
|
`${this.apiEndpoint}/security/scan/${scanId}`,
|
|
{
|
|
method: 'GET',
|
|
headers: this.http.getHeaders(),
|
|
}
|
|
)
|
|
|
|
return response.data
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Utility
|
|
// ---------------------------------------------------------------------------
|
|
|
|
cancelAllRequests(): void {
|
|
this.http.cancelAllRequests()
|
|
}
|
|
|
|
setTenantId(tenantId: string): void {
|
|
this.http.setTenantId(tenantId)
|
|
}
|
|
|
|
getTenantId(): string {
|
|
return this.http.getTenantId()
|
|
}
|
|
|
|
async healthCheck(): Promise<boolean> {
|
|
try {
|
|
const response = await this.http.fetchWithTimeout(
|
|
`${this.apiEndpoint}/health`,
|
|
{ method: 'GET' },
|
|
`health-${Date.now()}`
|
|
)
|
|
return response.ok
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// FACTORY
|
|
// =============================================================================
|
|
|
|
let clientInstance: ComplianceClient | null = null
|
|
|
|
export function getComplianceClient(options?: ComplianceClientOptions): ComplianceClient {
|
|
if (!clientInstance && !options) {
|
|
throw new Error('ComplianceClient not initialized. Provide options on first call.')
|
|
}
|
|
|
|
if (!clientInstance && options) {
|
|
clientInstance = new ComplianceClient(options)
|
|
}
|
|
|
|
return clientInstance!
|
|
}
|
|
|
|
export function resetComplianceClient(): void {
|
|
if (clientInstance) {
|
|
clientInstance.cancelAllRequests()
|
|
}
|
|
clientInstance = null
|
|
}
|