Files
breakpilot-compliance/breakpilot-compliance-sdk/packages/core/src/client.ts
Sharang Parnerkar 5cb91e88d2 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>
2026-04-11 22:39:47 +02:00

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
}