refactor(admin): split api-client.ts (885 LOC) and endpoints.ts (1262 LOC) into focused modules

api-client.ts is now a thin delegating class (263 LOC) backed by:
  - api-client-types.ts (84) — shared types, config, FetchContext
  - api-client-state.ts (120) — state CRUD + export
  - api-client-projects.ts (160) — project management
  - api-client-wiki.ts (116) — wiki knowledge base
  - api-client-operations.ts (299) — checkpoints, flow, modules, UCCA, import, screening

endpoints.ts is now a barrel (25 LOC) aggregating the 4 existing domain files
(endpoints-python-core, endpoints-python-gdpr, endpoints-python-ops, endpoints-go).

All files stay under the 500-line hard cap. Build verified with `npx next build`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sharang Parnerkar
2026-04-10 19:17:38 +02:00
parent 58e95d5e8e
commit e07e1de6c9
7 changed files with 875 additions and 1955 deletions

View File

@@ -0,0 +1,299 @@
/**
* SDK API Client — Operational methods.
* (checkpoints, flow, modules, UCCA, document import, screening, health)
*/
import {
APIResponse,
CheckpointValidationResult,
FetchContext,
CheckpointStatus,
} from './api-client-types'
// ---------------------------------------------------------------------------
// Checkpoint Validation
// ---------------------------------------------------------------------------
/**
* Validate a specific checkpoint
*/
export async function validateCheckpoint(
ctx: FetchContext,
checkpointId: string,
data?: unknown
): Promise<CheckpointValidationResult> {
const response = await ctx.fetchWithRetry<APIResponse<CheckpointValidationResult>>(
`${ctx.baseUrl}/checkpoints/validate`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tenantId: ctx.tenantId,
checkpointId,
data,
}),
}
)
if (!response.success || !response.data) {
throw ctx.createError(response.error || 'Checkpoint validation failed', 500, true)
}
return response.data
}
/**
* Get all checkpoint statuses
*/
export async function getCheckpoints(
ctx: FetchContext
): Promise<Record<string, CheckpointStatus>> {
const response = await ctx.fetchWithRetry<APIResponse<Record<string, CheckpointStatus>>>(
`${ctx.baseUrl}/checkpoints?tenantId=${encodeURIComponent(ctx.tenantId)}`,
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}
)
return response.data || {}
}
// ---------------------------------------------------------------------------
// Flow Navigation
// ---------------------------------------------------------------------------
/**
* Get current flow state
*/
export async function getFlowState(ctx: FetchContext): Promise<{
currentStep: string
currentPhase: 1 | 2
completedSteps: string[]
suggestions: Array<{ stepId: string; reason: string }>
}> {
const response = await ctx.fetchWithRetry<APIResponse<{
currentStep: string
currentPhase: 1 | 2
completedSteps: string[]
suggestions: Array<{ stepId: string; reason: string }>
}>>(
`${ctx.baseUrl}/flow?tenantId=${encodeURIComponent(ctx.tenantId)}`,
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}
)
if (!response.data) {
throw ctx.createError('Failed to get flow state', 500, true)
}
return response.data
}
/**
* Navigate to next/previous step
*/
export async function navigateFlow(
ctx: FetchContext,
direction: 'next' | 'previous'
): Promise<{ stepId: string; phase: 1 | 2 }> {
const response = await ctx.fetchWithRetry<APIResponse<{
stepId: string
phase: 1 | 2
}>>(
`${ctx.baseUrl}/flow`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tenantId: ctx.tenantId,
direction,
}),
}
)
if (!response.data) {
throw ctx.createError('Failed to navigate flow', 500, true)
}
return response.data
}
// ---------------------------------------------------------------------------
// Modules
// ---------------------------------------------------------------------------
/**
* Get available compliance modules from backend
*/
export async function getModules(
ctx: FetchContext,
filters?: {
serviceType?: string
criticality?: string
processesPii?: boolean
aiComponents?: boolean
}
): Promise<{ modules: unknown[]; total: number }> {
const params = new URLSearchParams()
if (filters?.serviceType) params.set('service_type', filters.serviceType)
if (filters?.criticality) params.set('criticality', filters.criticality)
if (filters?.processesPii !== undefined) params.set('processes_pii', String(filters.processesPii))
if (filters?.aiComponents !== undefined) params.set('ai_components', String(filters.aiComponents))
const queryString = params.toString()
const url = `${ctx.baseUrl}/modules${queryString ? `?${queryString}` : ''}`
const response = await ctx.fetchWithRetry<{ modules: unknown[]; total: number }>(
url,
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}
)
return response
}
// ---------------------------------------------------------------------------
// UCCA (Use Case Compliance Assessment)
// ---------------------------------------------------------------------------
/**
* Assess a use case
*/
export async function assessUseCase(
ctx: FetchContext,
intake: unknown
): Promise<unknown> {
const response = await ctx.fetchWithRetry<APIResponse<unknown>>(
`${ctx.baseUrl}/ucca/assess`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': ctx.tenantId,
},
body: JSON.stringify(intake),
}
)
return response
}
/**
* Get all assessments
*/
export async function getAssessments(ctx: FetchContext): Promise<unknown[]> {
const response = await ctx.fetchWithRetry<APIResponse<unknown[]>>(
`${ctx.baseUrl}/ucca/assessments?tenantId=${encodeURIComponent(ctx.tenantId)}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': ctx.tenantId,
},
}
)
return response.data || []
}
/**
* Get a single assessment
*/
export async function getAssessment(
ctx: FetchContext,
id: string
): Promise<unknown> {
const response = await ctx.fetchWithRetry<APIResponse<unknown>>(
`${ctx.baseUrl}/ucca/assessments/${id}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': ctx.tenantId,
},
}
)
return response.data
}
/**
* Delete an assessment
*/
export async function deleteAssessment(
ctx: FetchContext,
id: string
): Promise<void> {
await ctx.fetchWithRetry<APIResponse<void>>(
`${ctx.baseUrl}/ucca/assessments/${id}`,
{
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': ctx.tenantId,
},
}
)
}
// ---------------------------------------------------------------------------
// Document Import & Screening
// ---------------------------------------------------------------------------
/**
* Analyze an uploaded document
*/
export async function analyzeDocument(
ctx: FetchContext,
formData: FormData
): Promise<unknown> {
const response = await ctx.fetchWithRetry<APIResponse<unknown>>(
`${ctx.baseUrl}/import/analyze`,
{
method: 'POST',
headers: { 'X-Tenant-ID': ctx.tenantId },
body: formData,
}
)
return response.data
}
/**
* Scan a dependency file (package-lock.json, requirements.txt, etc.)
*/
export async function scanDependencies(
ctx: FetchContext,
formData: FormData
): Promise<unknown> {
const response = await ctx.fetchWithRetry<APIResponse<unknown>>(
`${ctx.baseUrl}/screening/scan`,
{
method: 'POST',
headers: { 'X-Tenant-ID': ctx.tenantId },
body: formData,
}
)
return response.data
}
// ---------------------------------------------------------------------------
// Health
// ---------------------------------------------------------------------------
/**
* Health check
*/
export async function healthCheck(ctx: FetchContext): Promise<boolean> {
try {
const response = await ctx.fetchWithTimeout(
`${ctx.baseUrl}/health`,
{ method: 'GET' },
`health-${Date.now()}`
)
return response.ok
} catch {
return false
}
}

View File

@@ -0,0 +1,160 @@
/**
* SDK API Client — Project management methods.
* (listProjects, createProject, updateProject, getProject,
* archiveProject, restoreProject, permanentlyDeleteProject)
*/
import { FetchContext } from './api-client-types'
import { ProjectInfo } from './types'
/**
* List all projects for the current tenant
*/
export async function listProjects(
ctx: FetchContext,
includeArchived = true
): Promise<{ projects: ProjectInfo[]; total: number }> {
const response = await ctx.fetchWithRetry<{ projects: ProjectInfo[]; total: number }>(
`${ctx.baseUrl}/projects?tenant_id=${encodeURIComponent(ctx.tenantId)}&include_archived=${includeArchived}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': ctx.tenantId,
},
}
)
return response
}
/**
* Create a new project
*/
export async function createProject(
ctx: FetchContext,
data: {
name: string
description?: string
customer_type?: string
copy_from_project_id?: string
}
): Promise<ProjectInfo> {
const response = await ctx.fetchWithRetry<ProjectInfo>(
`${ctx.baseUrl}/projects?tenant_id=${encodeURIComponent(ctx.tenantId)}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': ctx.tenantId,
},
body: JSON.stringify({
...data,
tenant_id: ctx.tenantId,
}),
}
)
return response
}
/**
* Update an existing project
*/
export async function updateProject(
ctx: FetchContext,
projectId: string,
data: { name?: string; description?: string }
): Promise<ProjectInfo> {
const response = await ctx.fetchWithRetry<ProjectInfo>(
`${ctx.baseUrl}/projects/${projectId}?tenant_id=${encodeURIComponent(ctx.tenantId)}`,
{
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': ctx.tenantId,
},
body: JSON.stringify({
...data,
tenant_id: ctx.tenantId,
}),
}
)
return response
}
/**
* Get a single project by ID
*/
export async function getProject(
ctx: FetchContext,
projectId: string
): Promise<ProjectInfo> {
const response = await ctx.fetchWithRetry<ProjectInfo>(
`${ctx.baseUrl}/projects/${projectId}?tenant_id=${encodeURIComponent(ctx.tenantId)}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': ctx.tenantId,
},
}
)
return response
}
/**
* Archive (soft-delete) a project
*/
export async function archiveProject(
ctx: FetchContext,
projectId: string
): Promise<void> {
await ctx.fetchWithRetry<{ success: boolean }>(
`${ctx.baseUrl}/projects/${projectId}?tenant_id=${encodeURIComponent(ctx.tenantId)}`,
{
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': ctx.tenantId,
},
}
)
}
/**
* Restore an archived project
*/
export async function restoreProject(
ctx: FetchContext,
projectId: string
): Promise<ProjectInfo> {
const response = await ctx.fetchWithRetry<ProjectInfo>(
`${ctx.baseUrl}/projects/${projectId}/restore?tenant_id=${encodeURIComponent(ctx.tenantId)}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': ctx.tenantId,
},
}
)
return response
}
/**
* Permanently delete a project and all data
*/
export async function permanentlyDeleteProject(
ctx: FetchContext,
projectId: string
): Promise<void> {
await ctx.fetchWithRetry<{ success: boolean }>(
`${ctx.baseUrl}/projects/${projectId}/permanent?tenant_id=${encodeURIComponent(ctx.tenantId)}`,
{
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': ctx.tenantId,
},
}
)
}

View File

@@ -0,0 +1,120 @@
/**
* SDK API Client — State management methods.
* (getState, saveState, deleteState, exportState)
*/
import {
APIResponse,
APIError,
StateResponse,
FetchContext,
SDKState,
} from './api-client-types'
/**
* Load SDK state for the current tenant
*/
export async function getState(ctx: FetchContext): Promise<StateResponse | null> {
try {
const params = new URLSearchParams({ tenantId: ctx.tenantId })
if (ctx.projectId) params.set('projectId', ctx.projectId)
const response = await ctx.fetchWithRetry<APIResponse<StateResponse>>(
`${ctx.baseUrl}/state?${params.toString()}`,
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}
)
if (response.success && response.data) {
return response.data
}
return null
} catch (error) {
const apiError = error as APIError
// 404 means no state exists yet - that's okay
if (apiError.status === 404) {
return null
}
throw error
}
}
/**
* Save SDK state for the current tenant.
* Supports optimistic locking via version parameter.
*/
export async function saveState(
ctx: FetchContext,
state: SDKState,
version?: number
): Promise<StateResponse> {
const response = await ctx.fetchWithRetry<APIResponse<StateResponse>>(
`${ctx.baseUrl}/state`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(version !== undefined && { 'If-Match': String(version) }),
},
body: JSON.stringify({
tenantId: ctx.tenantId,
projectId: ctx.projectId,
state,
version,
}),
}
)
if (!response.success) {
throw ctx.createError(response.error || 'Failed to save state', 500, true)
}
return response.data!
}
/**
* Delete SDK state for the current tenant
*/
export async function deleteState(ctx: FetchContext): Promise<void> {
const params = new URLSearchParams({ tenantId: ctx.tenantId })
if (ctx.projectId) params.set('projectId', ctx.projectId)
await ctx.fetchWithRetry<APIResponse<void>>(
`${ctx.baseUrl}/state?${params.toString()}`,
{
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
}
)
}
/**
* Export SDK state in various formats
*/
export async function exportState(
ctx: FetchContext,
format: 'json' | 'pdf' | 'zip'
): Promise<Blob> {
const response = await ctx.fetchWithTimeout(
`${ctx.baseUrl}/export?tenantId=${encodeURIComponent(ctx.tenantId)}&format=${format}`,
{
method: 'GET',
headers: {
'Accept':
format === 'json'
? 'application/json'
: format === 'pdf'
? 'application/pdf'
: 'application/zip',
},
},
`export-${Date.now()}`
)
if (!response.ok) {
throw ctx.createError(`Export failed: ${response.statusText}`, response.status, true)
}
return response.blob()
}

View File

@@ -0,0 +1,84 @@
/**
* SDK API Client — shared types, interfaces, and configuration constants.
*/
import { SDKState, CheckpointStatus } from './types'
// =============================================================================
// TYPES
// =============================================================================
export interface APIResponse<T> {
success: boolean
data?: T
error?: string
version?: number
lastModified?: string
}
export interface StateResponse {
tenantId: string
state: SDKState
version: number
lastModified: string
}
export interface SaveStateRequest {
tenantId: string
state: SDKState
version?: number // For optimistic locking
}
export interface CheckpointValidationResult {
checkpointId: string
passed: boolean
errors: Array<{
ruleId: string
field: string
message: string
severity: 'ERROR' | 'WARNING' | 'INFO'
}>
warnings: Array<{
ruleId: string
field: string
message: string
severity: 'ERROR' | 'WARNING' | 'INFO'
}>
validatedAt: string
validatedBy: string
}
export interface APIError extends Error {
status?: number
code?: string
retryable: boolean
}
// =============================================================================
// CONFIGURATION
// =============================================================================
export const DEFAULT_BASE_URL = '/api/sdk/v1'
export const DEFAULT_TIMEOUT = 30000 // 30 seconds
export const MAX_RETRIES = 3
export const RETRY_DELAYS = [1000, 2000, 4000] // Exponential backoff
// =============================================================================
// FETCH CONTEXT — passed to domain helpers
// =============================================================================
/**
* Subset of the SDKApiClient that domain helpers need to make requests.
* Avoids exposing the entire class and keeps helpers unit-testable.
*/
export interface FetchContext {
baseUrl: string
tenantId: string
projectId: string | undefined
fetchWithRetry<T>(url: string, options: RequestInit, retries?: number): Promise<T>
fetchWithTimeout(url: string, options: RequestInit, requestId: string): Promise<Response>
createError(message: string, status?: number, retryable?: boolean): APIError
}
// Re-export types that domain helpers need from ./types
export type { SDKState, CheckpointStatus }

View File

@@ -0,0 +1,116 @@
/**
* SDK API Client — Wiki (read-only knowledge base) methods.
* (listWikiCategories, listWikiArticles, getWikiArticle, searchWiki)
*/
import { FetchContext } from './api-client-types'
import { WikiCategory, WikiArticle, WikiSearchResult } from './types'
/**
* List all wiki categories with article counts
*/
export async function listWikiCategories(ctx: FetchContext): Promise<WikiCategory[]> {
const data = await ctx.fetchWithRetry<{ categories: Array<{
id: string; name: string; description: string; icon: string;
sort_order: number; article_count: number
}> }>(
`${ctx.baseUrl}/wiki?endpoint=categories`,
{ method: 'GET' }
)
return (data.categories || []).map(c => ({
id: c.id,
name: c.name,
description: c.description,
icon: c.icon,
sortOrder: c.sort_order,
articleCount: c.article_count,
}))
}
/**
* List wiki articles, optionally filtered by category
*/
export async function listWikiArticles(
ctx: FetchContext,
categoryId?: string
): Promise<WikiArticle[]> {
const params = new URLSearchParams({ endpoint: 'articles' })
if (categoryId) params.set('category_id', categoryId)
const data = await ctx.fetchWithRetry<{ articles: Array<{
id: string; category_id: string; category_name: string; title: string;
summary: string; content: string; legal_refs: string[]; tags: string[];
relevance: string; source_urls: string[]; version: number; updated_at: string
}> }>(
`${ctx.baseUrl}/wiki?${params.toString()}`,
{ method: 'GET' }
)
return (data.articles || []).map(a => ({
id: a.id,
categoryId: a.category_id,
categoryName: a.category_name,
title: a.title,
summary: a.summary,
content: a.content,
legalRefs: a.legal_refs || [],
tags: a.tags || [],
relevance: a.relevance as WikiArticle['relevance'],
sourceUrls: a.source_urls || [],
version: a.version,
updatedAt: a.updated_at,
}))
}
/**
* Get a single wiki article by ID
*/
export async function getWikiArticle(
ctx: FetchContext,
id: string
): Promise<WikiArticle> {
const data = await ctx.fetchWithRetry<{
id: string; category_id: string; category_name: string; title: string;
summary: string; content: string; legal_refs: string[]; tags: string[];
relevance: string; source_urls: string[]; version: number; updated_at: string
}>(
`${ctx.baseUrl}/wiki?endpoint=article&id=${encodeURIComponent(id)}`,
{ method: 'GET' }
)
return {
id: data.id,
categoryId: data.category_id,
categoryName: data.category_name,
title: data.title,
summary: data.summary,
content: data.content,
legalRefs: data.legal_refs || [],
tags: data.tags || [],
relevance: data.relevance as WikiArticle['relevance'],
sourceUrls: data.source_urls || [],
version: data.version,
updatedAt: data.updated_at,
}
}
/**
* Full-text search across wiki articles
*/
export async function searchWiki(
ctx: FetchContext,
query: string
): Promise<WikiSearchResult[]> {
const data = await ctx.fetchWithRetry<{ results: Array<{
id: string; title: string; summary: string; category_name: string;
relevance: string; highlight: string
}> }>(
`${ctx.baseUrl}/wiki?endpoint=search&q=${encodeURIComponent(query)}`,
{ method: 'GET' }
)
return (data.results || []).map(r => ({
id: r.id,
title: r.title,
summary: r.summary,
categoryName: r.category_name,
relevance: r.relevance,
highlight: r.highlight,
}))
}

View File

@@ -3,68 +3,36 @@
*
* Centralized API client for SDK state management with error handling,
* retry logic, and optimistic locking support.
*
* Domain methods are implemented in sibling files and delegated to here:
* api-client-state.ts — getState, saveState, deleteState, exportState
* api-client-projects.ts — listProjects … permanentlyDeleteProject
* api-client-wiki.ts — listWikiCategories … searchWiki
* api-client-operations.ts — checkpoints, flow, modules, UCCA, import, screening
*/
import { SDKState, CheckpointStatus, ProjectInfo, WikiCategory, WikiArticle, WikiSearchResult } from './types'
import {
APIResponse,
StateResponse,
SaveStateRequest,
CheckpointValidationResult,
APIError,
FetchContext,
DEFAULT_BASE_URL,
DEFAULT_TIMEOUT,
MAX_RETRIES,
RETRY_DELAYS,
} from './api-client-types'
// =============================================================================
// TYPES
// =============================================================================
// Re-export public types so existing consumers keep working
export type { APIResponse, StateResponse, SaveStateRequest, CheckpointValidationResult, APIError }
export interface APIResponse<T> {
success: boolean
data?: T
error?: string
version?: number
lastModified?: string
}
export interface StateResponse {
tenantId: string
state: SDKState
version: number
lastModified: string
}
export interface SaveStateRequest {
tenantId: string
state: SDKState
version?: number // For optimistic locking
}
export interface CheckpointValidationResult {
checkpointId: string
passed: boolean
errors: Array<{
ruleId: string
field: string
message: string
severity: 'ERROR' | 'WARNING' | 'INFO'
}>
warnings: Array<{
ruleId: string
field: string
message: string
severity: 'ERROR' | 'WARNING' | 'INFO'
}>
validatedAt: string
validatedBy: string
}
export interface APIError extends Error {
status?: number
code?: string
retryable: boolean
}
// =============================================================================
// CONFIGURATION
// =============================================================================
const DEFAULT_BASE_URL = '/api/sdk/v1'
const DEFAULT_TIMEOUT = 30000 // 30 seconds
const MAX_RETRIES = 3
const RETRY_DELAYS = [1000, 2000, 4000] // Exponential backoff
// Domain helpers
import * as stateHelpers from './api-client-state'
import * as projectHelpers from './api-client-projects'
import * as wikiHelpers from './api-client-wiki'
import * as opsHelpers from './api-client-operations'
// =============================================================================
// API CLIENT
@@ -90,17 +58,17 @@ export class SDKApiClient {
}
// ---------------------------------------------------------------------------
// Private Methods
// Private infrastructure — also exposed via FetchContext to helpers
// ---------------------------------------------------------------------------
private createError(message: string, status?: number, retryable = false): APIError {
createError(message: string, status?: number, retryable = false): APIError {
const error = new Error(message) as APIError
error.status = status
error.retryable = retryable
return error
}
private async fetchWithTimeout(
async fetchWithTimeout(
url: string,
options: RequestInit,
requestId: string
@@ -122,7 +90,7 @@ export class SDKApiClient {
}
}
private async fetchWithRetry<T>(
async fetchWithRetry<T>(
url: string,
options: RequestInit,
retries = MAX_RETRIES
@@ -182,673 +150,83 @@ export class SDKApiClient {
return new Promise(resolve => setTimeout(resolve, ms))
}
// ---------------------------------------------------------------------------
// Public Methods - State Management
// ---------------------------------------------------------------------------
/**
* Load SDK state for the current tenant
*/
async getState(): Promise<StateResponse | null> {
try {
const params = new URLSearchParams({ tenantId: this.tenantId })
if (this.projectId) params.set('projectId', this.projectId)
const response = await this.fetchWithRetry<APIResponse<StateResponse>>(
`${this.baseUrl}/state?${params.toString()}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
}
)
if (response.success && response.data) {
return response.data
}
return null
} catch (error) {
const apiError = error as APIError
// 404 means no state exists yet - that's okay
if (apiError.status === 404) {
return null
}
throw error
/** Build a FetchContext for passing to domain helpers */
private get ctx(): FetchContext {
return {
baseUrl: this.baseUrl,
tenantId: this.tenantId,
projectId: this.projectId,
fetchWithRetry: this.fetchWithRetry.bind(this),
fetchWithTimeout: this.fetchWithTimeout.bind(this),
createError: this.createError.bind(this),
}
}
/**
* Save SDK state for the current tenant
* Supports optimistic locking via version parameter
*/
async saveState(state: SDKState, version?: number): Promise<StateResponse> {
const response = await this.fetchWithRetry<APIResponse<StateResponse>>(
`${this.baseUrl}/state`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(version !== undefined && { 'If-Match': String(version) }),
},
body: JSON.stringify({
tenantId: this.tenantId,
projectId: this.projectId,
state,
version,
}),
}
)
if (!response.success) {
throw this.createError(response.error || 'Failed to save state', 500, true)
}
return response.data!
}
/**
* Delete SDK state for the current tenant
*/
async deleteState(): Promise<void> {
const params = new URLSearchParams({ tenantId: this.tenantId })
if (this.projectId) params.set('projectId', this.projectId)
await this.fetchWithRetry<APIResponse<void>>(
`${this.baseUrl}/state?${params.toString()}`,
{
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
}
)
}
// ---------------------------------------------------------------------------
// Public Methods - Checkpoint Validation
// State Management (api-client-state.ts)
// ---------------------------------------------------------------------------
/**
* Validate a specific checkpoint
*/
async validateCheckpoint(
checkpointId: string,
data?: unknown
): Promise<CheckpointValidationResult> {
const response = await this.fetchWithRetry<APIResponse<CheckpointValidationResult>>(
`${this.baseUrl}/checkpoints/validate`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
tenantId: this.tenantId,
checkpointId,
data,
}),
}
)
if (!response.success || !response.data) {
throw this.createError(response.error || 'Checkpoint validation failed', 500, true)
}
return response.data
}
/**
* Get all checkpoint statuses
*/
async getCheckpoints(): Promise<Record<string, CheckpointStatus>> {
const response = await this.fetchWithRetry<APIResponse<Record<string, CheckpointStatus>>>(
`${this.baseUrl}/checkpoints?tenantId=${encodeURIComponent(this.tenantId)}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
}
)
return response.data || {}
}
async getState(): Promise<StateResponse | null> { return stateHelpers.getState(this.ctx) }
async saveState(state: SDKState, version?: number): Promise<StateResponse> { return stateHelpers.saveState(this.ctx, state, version) }
async deleteState(): Promise<void> { return stateHelpers.deleteState(this.ctx) }
async exportState(format: 'json' | 'pdf' | 'zip'): Promise<Blob> { return stateHelpers.exportState(this.ctx, format) }
// ---------------------------------------------------------------------------
// Public Methods - Flow Navigation
// Checkpoints & Flow (api-client-operations.ts)
// ---------------------------------------------------------------------------
/**
* Get current flow state
*/
async getFlowState(): Promise<{
currentStep: string
currentPhase: 1 | 2
completedSteps: string[]
suggestions: Array<{ stepId: string; reason: string }>
}> {
const response = await this.fetchWithRetry<APIResponse<{
currentStep: string
currentPhase: 1 | 2
completedSteps: string[]
suggestions: Array<{ stepId: string; reason: string }>
}>>(
`${this.baseUrl}/flow?tenantId=${encodeURIComponent(this.tenantId)}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
}
)
if (!response.data) {
throw this.createError('Failed to get flow state', 500, true)
}
return response.data
}
/**
* Navigate to next/previous step
*/
async navigateFlow(direction: 'next' | 'previous'): Promise<{
stepId: string
phase: 1 | 2
}> {
const response = await this.fetchWithRetry<APIResponse<{
stepId: string
phase: 1 | 2
}>>(
`${this.baseUrl}/flow`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
tenantId: this.tenantId,
direction,
}),
}
)
if (!response.data) {
throw this.createError('Failed to navigate flow', 500, true)
}
return response.data
}
async validateCheckpoint(checkpointId: string, data?: unknown): Promise<CheckpointValidationResult> { return opsHelpers.validateCheckpoint(this.ctx, checkpointId, data) }
async getCheckpoints(): Promise<Record<string, CheckpointStatus>> { return opsHelpers.getCheckpoints(this.ctx) }
async getFlowState() { return opsHelpers.getFlowState(this.ctx) }
async navigateFlow(direction: 'next' | 'previous') { return opsHelpers.navigateFlow(this.ctx, direction) }
// ---------------------------------------------------------------------------
// Public Methods - Modules
// Modules, UCCA, Import, Screening, Health (api-client-operations.ts)
// ---------------------------------------------------------------------------
/**
* Get available compliance modules from backend
*/
async getModules(filters?: {
serviceType?: string
criticality?: string
processesPii?: boolean
aiComponents?: boolean
}): Promise<{ modules: unknown[]; total: number }> {
const params = new URLSearchParams()
if (filters?.serviceType) params.set('service_type', filters.serviceType)
if (filters?.criticality) params.set('criticality', filters.criticality)
if (filters?.processesPii !== undefined) params.set('processes_pii', String(filters.processesPii))
if (filters?.aiComponents !== undefined) params.set('ai_components', String(filters.aiComponents))
const queryString = params.toString()
const url = `${this.baseUrl}/modules${queryString ? `?${queryString}` : ''}`
const response = await this.fetchWithRetry<{ modules: unknown[]; total: number }>(
url,
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}
)
return response
}
async getModules(filters?: Parameters<typeof opsHelpers.getModules>[1]) { return opsHelpers.getModules(this.ctx, filters) }
async assessUseCase(intake: unknown) { return opsHelpers.assessUseCase(this.ctx, intake) }
async getAssessments() { return opsHelpers.getAssessments(this.ctx) }
async getAssessment(id: string) { return opsHelpers.getAssessment(this.ctx, id) }
async deleteAssessment(id: string) { return opsHelpers.deleteAssessment(this.ctx, id) }
async analyzeDocument(formData: FormData) { return opsHelpers.analyzeDocument(this.ctx, formData) }
async scanDependencies(formData: FormData) { return opsHelpers.scanDependencies(this.ctx, formData) }
async healthCheck() { return opsHelpers.healthCheck(this.ctx) }
// ---------------------------------------------------------------------------
// Public Methods - UCCA (Use Case Compliance Assessment)
// Projects (api-client-projects.ts)
// ---------------------------------------------------------------------------
/**
* Assess a use case
*/
async assessUseCase(intake: unknown): Promise<unknown> {
const response = await this.fetchWithRetry<APIResponse<unknown>>(
`${this.baseUrl}/ucca/assess`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': this.tenantId,
},
body: JSON.stringify(intake),
}
)
return response
}
/**
* Get all assessments
*/
async getAssessments(): Promise<unknown[]> {
const response = await this.fetchWithRetry<APIResponse<unknown[]>>(
`${this.baseUrl}/ucca/assessments?tenantId=${encodeURIComponent(this.tenantId)}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': this.tenantId,
},
}
)
return response.data || []
}
/**
* Get a single assessment
*/
async getAssessment(id: string): Promise<unknown> {
const response = await this.fetchWithRetry<APIResponse<unknown>>(
`${this.baseUrl}/ucca/assessments/${id}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': this.tenantId,
},
}
)
return response.data
}
/**
* Delete an assessment
*/
async deleteAssessment(id: string): Promise<void> {
await this.fetchWithRetry<APIResponse<void>>(
`${this.baseUrl}/ucca/assessments/${id}`,
{
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': this.tenantId,
},
}
)
}
async listProjects(includeArchived = true) { return projectHelpers.listProjects(this.ctx, includeArchived) }
async createProject(data: Parameters<typeof projectHelpers.createProject>[1]) { return projectHelpers.createProject(this.ctx, data) }
async updateProject(projectId: string, data: Parameters<typeof projectHelpers.updateProject>[2]) { return projectHelpers.updateProject(this.ctx, projectId, data) }
async getProject(projectId: string) { return projectHelpers.getProject(this.ctx, projectId) }
async archiveProject(projectId: string) { return projectHelpers.archiveProject(this.ctx, projectId) }
async restoreProject(projectId: string) { return projectHelpers.restoreProject(this.ctx, projectId) }
async permanentlyDeleteProject(projectId: string) { return projectHelpers.permanentlyDeleteProject(this.ctx, projectId) }
// ---------------------------------------------------------------------------
// Public Methods - Document Import
// Wiki (api-client-wiki.ts)
// ---------------------------------------------------------------------------
/**
* Analyze an uploaded document
*/
async analyzeDocument(formData: FormData): Promise<unknown> {
const response = await this.fetchWithRetry<APIResponse<unknown>>(
`${this.baseUrl}/import/analyze`,
{
method: 'POST',
headers: {
'X-Tenant-ID': this.tenantId,
},
body: formData,
}
)
return response.data
}
async listWikiCategories() { return wikiHelpers.listWikiCategories(this.ctx) }
async listWikiArticles(categoryId?: string) { return wikiHelpers.listWikiArticles(this.ctx, categoryId) }
async getWikiArticle(id: string) { return wikiHelpers.getWikiArticle(this.ctx, id) }
async searchWiki(query: string) { return wikiHelpers.searchWiki(this.ctx, query) }
// ---------------------------------------------------------------------------
// Public Methods - System Screening
// Utility
// ---------------------------------------------------------------------------
/**
* Scan a dependency file (package-lock.json, requirements.txt, etc.)
*/
async scanDependencies(formData: FormData): Promise<unknown> {
const response = await this.fetchWithRetry<APIResponse<unknown>>(
`${this.baseUrl}/screening/scan`,
{
method: 'POST',
headers: {
'X-Tenant-ID': this.tenantId,
},
body: formData,
}
)
return response.data
}
// ---------------------------------------------------------------------------
// Public Methods - Export
// ---------------------------------------------------------------------------
/**
* Export SDK state in various formats
*/
async exportState(format: 'json' | 'pdf' | 'zip'): Promise<Blob> {
const response = await this.fetchWithTimeout(
`${this.baseUrl}/export?tenantId=${encodeURIComponent(this.tenantId)}&format=${format}`,
{
method: 'GET',
headers: {
'Accept': format === 'json' ? 'application/json' : format === 'pdf' ? 'application/pdf' : 'application/zip',
},
},
`export-${Date.now()}`
)
if (!response.ok) {
throw this.createError(`Export failed: ${response.statusText}`, response.status, true)
}
return response.blob()
}
// ---------------------------------------------------------------------------
// Public Methods - Utility
// ---------------------------------------------------------------------------
/**
* Cancel all pending requests
*/
cancelAllRequests(): void {
this.abortControllers.forEach(controller => controller.abort())
this.abortControllers.clear()
}
/**
* Update tenant ID (useful when switching contexts)
*/
setTenantId(tenantId: string): void {
this.tenantId = tenantId
}
/**
* Get current tenant ID
*/
getTenantId(): string {
return this.tenantId
}
/**
* Set project ID for multi-project support
*/
setProjectId(projectId: string | undefined): void {
this.projectId = projectId
}
/**
* Get current project ID
*/
getProjectId(): string | undefined {
return this.projectId
}
// ---------------------------------------------------------------------------
// Public Methods - Project Management
// ---------------------------------------------------------------------------
/**
* List all projects for the current tenant
*/
async listProjects(includeArchived = true): Promise<{ projects: ProjectInfo[]; total: number }> {
const response = await this.fetchWithRetry<{ projects: ProjectInfo[]; total: number }>(
`${this.baseUrl}/projects?tenant_id=${encodeURIComponent(this.tenantId)}&include_archived=${includeArchived}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': this.tenantId,
},
}
)
return response
}
/**
* Create a new project
*/
async createProject(data: {
name: string
description?: string
customer_type?: string
copy_from_project_id?: string
}): Promise<ProjectInfo> {
const response = await this.fetchWithRetry<ProjectInfo>(
`${this.baseUrl}/projects?tenant_id=${encodeURIComponent(this.tenantId)}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': this.tenantId,
},
body: JSON.stringify({
...data,
tenant_id: this.tenantId,
}),
}
)
return response
}
/**
* Update an existing project
*/
async updateProject(projectId: string, data: {
name?: string
description?: string
}): Promise<ProjectInfo> {
const response = await this.fetchWithRetry<ProjectInfo>(
`${this.baseUrl}/projects/${projectId}?tenant_id=${encodeURIComponent(this.tenantId)}`,
{
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': this.tenantId,
},
body: JSON.stringify({
...data,
tenant_id: this.tenantId,
}),
}
)
return response
}
/**
* Get a single project by ID
*/
async getProject(projectId: string): Promise<ProjectInfo> {
const response = await this.fetchWithRetry<ProjectInfo>(
`${this.baseUrl}/projects/${projectId}?tenant_id=${encodeURIComponent(this.tenantId)}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': this.tenantId,
},
}
)
return response
}
/**
* Archive (soft-delete) a project
*/
async archiveProject(projectId: string): Promise<void> {
await this.fetchWithRetry<{ success: boolean }>(
`${this.baseUrl}/projects/${projectId}?tenant_id=${encodeURIComponent(this.tenantId)}`,
{
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': this.tenantId,
},
}
)
}
/**
* Restore an archived project
*/
async restoreProject(projectId: string): Promise<ProjectInfo> {
const response = await this.fetchWithRetry<ProjectInfo>(
`${this.baseUrl}/projects/${projectId}/restore?tenant_id=${encodeURIComponent(this.tenantId)}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': this.tenantId,
},
}
)
return response
}
/**
* Permanently delete a project and all data
*/
async permanentlyDeleteProject(projectId: string): Promise<void> {
await this.fetchWithRetry<{ success: boolean }>(
`${this.baseUrl}/projects/${projectId}/permanent?tenant_id=${encodeURIComponent(this.tenantId)}`,
{
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': this.tenantId,
},
}
)
}
// ===========================================================================
// WIKI (read-only knowledge base)
// ===========================================================================
/**
* List all wiki categories with article counts
*/
async listWikiCategories(): Promise<WikiCategory[]> {
const data = await this.fetchWithRetry<{ categories: Array<{
id: string; name: string; description: string; icon: string;
sort_order: number; article_count: number
}> }>(
`${this.baseUrl}/wiki?endpoint=categories`,
{ method: 'GET' }
)
return (data.categories || []).map(c => ({
id: c.id,
name: c.name,
description: c.description,
icon: c.icon,
sortOrder: c.sort_order,
articleCount: c.article_count,
}))
}
/**
* List wiki articles, optionally filtered by category
*/
async listWikiArticles(categoryId?: string): Promise<WikiArticle[]> {
const params = new URLSearchParams({ endpoint: 'articles' })
if (categoryId) params.set('category_id', categoryId)
const data = await this.fetchWithRetry<{ articles: Array<{
id: string; category_id: string; category_name: string; title: string;
summary: string; content: string; legal_refs: string[]; tags: string[];
relevance: string; source_urls: string[]; version: number; updated_at: string
}> }>(
`${this.baseUrl}/wiki?${params.toString()}`,
{ method: 'GET' }
)
return (data.articles || []).map(a => ({
id: a.id,
categoryId: a.category_id,
categoryName: a.category_name,
title: a.title,
summary: a.summary,
content: a.content,
legalRefs: a.legal_refs || [],
tags: a.tags || [],
relevance: a.relevance as WikiArticle['relevance'],
sourceUrls: a.source_urls || [],
version: a.version,
updatedAt: a.updated_at,
}))
}
/**
* Get a single wiki article by ID
*/
async getWikiArticle(id: string): Promise<WikiArticle> {
const data = await this.fetchWithRetry<{
id: string; category_id: string; category_name: string; title: string;
summary: string; content: string; legal_refs: string[]; tags: string[];
relevance: string; source_urls: string[]; version: number; updated_at: string
}>(
`${this.baseUrl}/wiki?endpoint=article&id=${encodeURIComponent(id)}`,
{ method: 'GET' }
)
return {
id: data.id,
categoryId: data.category_id,
categoryName: data.category_name,
title: data.title,
summary: data.summary,
content: data.content,
legalRefs: data.legal_refs || [],
tags: data.tags || [],
relevance: data.relevance as WikiArticle['relevance'],
sourceUrls: data.source_urls || [],
version: data.version,
updatedAt: data.updated_at,
}
}
/**
* Full-text search across wiki articles
*/
async searchWiki(query: string): Promise<WikiSearchResult[]> {
const data = await this.fetchWithRetry<{ results: Array<{
id: string; title: string; summary: string; category_name: string;
relevance: string; highlight: string
}> }>(
`${this.baseUrl}/wiki?endpoint=search&q=${encodeURIComponent(query)}`,
{ method: 'GET' }
)
return (data.results || []).map(r => ({
id: r.id,
title: r.title,
summary: r.summary,
categoryName: r.category_name,
relevance: r.relevance,
highlight: r.highlight,
}))
}
/**
* Health check
*/
async healthCheck(): Promise<boolean> {
try {
const response = await this.fetchWithTimeout(
`${this.baseUrl}/health`,
{ method: 'GET' },
`health-${Date.now()}`
)
return response.ok
} catch {
return false
}
}
setTenantId(tenantId: string): void { this.tenantId = tenantId }
getTenantId(): string { return this.tenantId }
setProjectId(projectId: string | undefined): void { this.projectId = projectId }
getProjectId(): string | undefined { return this.projectId }
}
// =============================================================================

File diff suppressed because it is too large Load Diff