import { ApiError, apiFetch, apiFetchBlob } from "./client"; import { sanitizeCanonicalWorkspace } from "../workspaces/drafts"; export type WorkspaceErrorCode = | "workspace_invalid" | "binding_missing" | "workspace_not_activatable" | "workspace_stale" | "workspace_conflict" | "git_unavailable" | "git_auth_failed" | "git_non_fast_forward" | "git_push_rejected" | "connector_unavailable" | "semantic_index_incompatible"; export interface RestDiagnosticRequest { method: "GET" | "POST"; path: string; auth: "none" | "bearer" | "x-api-key"; } export interface CanonicalDiagnostics { dwh_rest?: RestDiagnosticRequest & { response: { database: string; schema: string } }; } export interface EvidencePolicy { max_chunk_chars: number; retain_published_generations: number; } export type EvidenceSource = | { type: "filesystem"; uri: string; patterns: string[]; max_bytes: number; } | { type: "http"; uris: string[]; authentication: "none" | "signed_urls_file"; connect_timeout_ms: number; read_timeout_ms: number; max_bytes: number; max_redirects: number; allow_private_hosts: boolean; max_cache_bytes: number; } | { type: "s3"; uri: string; endpoint_url?: string; region?: string; credentials: "ambient" | "static_files"; trusted_endpoint: boolean; allow_private_endpoint: boolean; allow_insecure_endpoint: boolean; max_bytes: number; max_objects: number; max_pages: number; page_size: number; }; export interface WorkspaceEvidence { source: EvidenceSource; policy: EvidencePolicy; } export interface CanonicalWorkspace { workspace: { schema_version: 3; id: string; name: string; description?: string; language: "en" | "it"; }; dwh: { engine: "postgres"; database: string; schema: string; port?: number; timeout_ms?: number; supported_transports: ("postgres_direct" | "rest_api" | "ssh_tunnel")[]; }; semantic_index: { vector_store: { engine: "qdrant"; collection: string; dimensions: 1024; distance: "cosine"; } embedding: { provider: "ollama_internal"; model: "qwen3-embedding:0.6b"; dimensions: 1024; }; }; llm_policy: { default?: `${string}/${string}`; allowed: `${string}/${string}`[] }; diagnostics?: CanonicalDiagnostics; evidence?: WorkspaceEvidence; } export interface WorkspaceRevision { id: string; commit: string; blob: string; snapshotPath: string; state: "operational" | "migration_required"; } export interface WorkspaceSummary { id: string; /** Kept for compatibility with the existing workspace selector. */ name: string; file: string; displayName: string; description?: string; language: "en" | "it"; revision: WorkspaceRevision; } export interface WorkspaceRecord { workspace: CanonicalWorkspace; revision: WorkspaceRevision; } export interface WorkspaceRegistryStatus { branch: string; head?: string; ahead: number; behind: number; degraded: boolean; lastError?: WorkspaceErrorCode; } export interface WorkspaceDiagnostic { level: "error" | "warning" | "info"; code: WorkspaceErrorCode | "binding_ok"; field?: string; message: string; } export interface WorkspaceDiagnostics { activatable: boolean; diagnostics: WorkspaceDiagnostic[]; } export type PublishWorkspaceRequest = | { action: "create"; workspace: CanonicalWorkspace; baseCommit: string } | { action: "update"; workspace: CanonicalWorkspace; baseCommit: string; baseBlob: string } | { action: "delete"; id: string; baseCommit: string; baseBlob: string }; export interface WorkspaceConflict { code: "workspace_conflict"; fields: string[]; expected: WorkspaceConflictRevision; actual: WorkspaceConflictRevision; base: CanonicalWorkspace; local: CanonicalWorkspace; remote: CanonicalWorkspace; } export interface WorkspaceConflictRevision { commit: string; blob?: string; } export interface WorkspaceApiError { status: number; code: WorkspaceErrorCode; message: string; fields?: string[]; } const conflictFields = new Set([ "workspace.schema_version", "workspace.id", "workspace.name", "workspace.description", "workspace.language", "dwh.engine", "dwh.database", "dwh.schema", "dwh.port", "dwh.timeout_ms", "dwh.supported_transports", "semantic_index.vector_store.engine", "semantic_index.vector_store.collection", "semantic_index.vector_store.dimensions", "semantic_index.vector_store.distance", "semantic_index.embedding.provider", "semantic_index.embedding.model", "semantic_index.embedding.dimensions", "llm_policy.default", "llm_policy.allowed", "diagnostics", "diagnostics.dwh_rest", "diagnostics.dwh_rest.method", "diagnostics.dwh_rest.path", "diagnostics.dwh_rest.auth", "diagnostics.dwh_rest.response.database", "diagnostics.dwh_rest.response.schema", "evidence", "evidence.source", "evidence.source.type", "evidence.source.uri", "evidence.source.patterns", "evidence.source.max_bytes", "evidence.source.uris", "evidence.source.authentication", "evidence.source.connect_timeout_ms", "evidence.source.read_timeout_ms", "evidence.source.max_redirects", "evidence.source.allow_private_hosts", "evidence.source.max_cache_bytes", "evidence.source.endpoint_url", "evidence.source.region", "evidence.source.credentials", "evidence.source.trusted_endpoint", "evidence.source.allow_private_endpoint", "evidence.source.allow_insecure_endpoint", "evidence.source.max_objects", "evidence.source.max_pages", "evidence.source.page_size", "evidence.policy", "evidence.policy.max_chunk_chars", "evidence.policy.retain_published_generations", ]); const workspaceErrorCodes = new Set([ "workspace_invalid", "binding_missing", "workspace_not_activatable", "workspace_stale", "workspace_conflict", "git_unavailable", "git_auth_failed", "git_non_fast_forward", "git_push_rejected", "connector_unavailable", "semantic_index_incompatible", ]); function object(value: unknown): Record | undefined { return value && typeof value === "object" && !Array.isArray(value) ? value as Record : undefined; } function conflictRevision(value: unknown): WorkspaceConflictRevision | undefined { const source = object(value); const commit = source?.commit; const blob = source?.blob; if (typeof commit !== "string" || !/^[0-9a-f]{40}$/.test(commit)) return undefined; if (blob !== undefined && (typeof blob !== "string" || !/^[0-9a-f]{40}$/.test(blob))) return undefined; return { commit, ...(typeof blob === "string" ? { blob } : {}) }; } /** Sanitized registry error data; it intentionally excludes the raw response body. */ export function asWorkspaceApiError(error: unknown): WorkspaceApiError | undefined { if (!(error instanceof ApiError)) return undefined; const payload = object(error.payload); const code = payload?.code; const message = payload?.message; if (typeof code !== "string" || !workspaceErrorCodes.has(code as WorkspaceErrorCode) || typeof message !== "string") { return undefined; } const fields = Array.isArray(payload?.fields) && payload.fields.every((field) => typeof field === "string") ? payload.fields : undefined; return { status: error.status, code: code as WorkspaceErrorCode, message, ...(fields ? { fields } : {}) }; } /** Narrows a sanitized 409 payload without exposing ApiError's raw body. */ export function asWorkspaceConflict(error: unknown): WorkspaceConflict | undefined { const safe = asWorkspaceApiError(error); if (safe?.code !== "workspace_conflict") return undefined; const payload = object((error as ApiError).payload); const fields = safe.fields; const base = payload && sanitizeCanonicalWorkspace(payload.base); const local = payload && sanitizeCanonicalWorkspace(payload.local); const remote = payload && sanitizeCanonicalWorkspace(payload.remote); const expected = payload && conflictRevision(payload.expected); const actual = payload && conflictRevision(payload.actual); if (!payload || !fields || !fields.every((field) => conflictFields.has(field)) || !expected || !actual || !base || !local || !remote) return undefined; return { code: "workspace_conflict", fields, expected, actual, base, local, remote, }; } function requireCanonicalWorkspace(value: unknown): CanonicalWorkspace { const workspace = sanitizeCanonicalWorkspace(value); if (!workspace) throw new Error("Workspace API returned an invalid canonical workspace"); return workspace; } export const listWorkspaces = () => apiFetch("/workspaces"); export const getWorkspace = async (id: string): Promise => { const response = await apiFetch(`/workspaces/${encodeURIComponent(id)}`); const source = object(response); if (!source) throw new Error("Workspace API returned an invalid workspace record"); return { workspace: requireCanonicalWorkspace(source.workspace), revision: source.revision as WorkspaceRevision, }; }; export const getWorkspaceRegistryStatus = () => apiFetch("/workspace-registry/status"); export const pullWorkspaceRegistry = () => apiFetch("/workspace-registry/pull", { method: "POST" }); export const validateWorkspace = async (workspace: CanonicalWorkspace) => { const safe = requireCanonicalWorkspace(workspace); const response = await apiFetch("/workspaces/validate", { method: "POST", body: JSON.stringify({ workspace: safe }), }); const source = object(response); if (!source) throw new Error("Workspace API returned an invalid validation result"); return { workspace: requireCanonicalWorkspace(source.workspace), contract: source.contract }; }; export const testWorkspace = (id: string) => apiFetch(`/workspaces/${encodeURIComponent(id)}/test`, { method: "POST" }); export const publishWorkspace = (request: PublishWorkspaceRequest) => { const safeRequest: PublishWorkspaceRequest = request.action === "delete" ? request : { ...request, workspace: requireCanonicalWorkspace(request.workspace) }; return apiFetch<{ revision: WorkspaceRevision } | undefined>("/workspaces/publish", { method: "POST", body: JSON.stringify(safeRequest), }); }; export const exportWorkspace = (id: string) => apiFetchBlob(`/workspaces/${encodeURIComponent(id)}/export`); export const importWorkspace = (bundle: File) => { const body = new FormData(); body.set("bundle", bundle); return apiFetch<{ draft: { workspace: CanonicalWorkspace; contract?: unknown } }>("/workspaces/import", { method: "POST", body, }); };