import { ApiError, apiFetch } from "./client"; import { sanitizeCanonicalWorkspace } from "../workspaces/drafts"; export type WorkspaceErrorCode = | "workspace_invalid" | "binding_missing" | "workspace_not_activatable" | "workspace_stale" | "git_unavailable" | "git_auth_failed" | "git_non_fast_forward" | "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; } export interface WorkspaceSummary { id: string; /** Kept for compatibility with the existing workspace selector. */ name: string; file: string; displayName: string; description?: string; configurationState: "ready" | "configuration_required"; revision?: WorkspaceRevision; } export interface WorkspaceRecord { workspace: CanonicalWorkspace; revision: WorkspaceRevision; } export interface WorkspaceRegistryStatus { branch: string; head?: string; ahead: number; behind: number; degraded: boolean; lastError?: WorkspaceErrorCode; repository?: { host: string; repository: string; transport: "https" | "ssh" | "local"; }; } export interface WorkspaceDiagnostic { level: "error" | "warning" | "info"; code: WorkspaceErrorCode | "binding_ok"; field?: string; message: string; } export interface WorkspaceDiagnostics { activatable: boolean; diagnostics: WorkspaceDiagnostic[]; } export interface WorkspaceSecretRequirement { id: string; connector: "dwh" | "evidence"; label: string; description: string; input: "password" | "textarea"; required: boolean; configured: boolean; } export interface WorkspaceRuntimeConfiguration { workspaceId: string; revision: WorkspaceRevision; configurationState: "ready" | "configuration_required"; requirements: WorkspaceSecretRequirement[]; } export interface WorkspaceApiError { status: number; code: WorkspaceErrorCode; message: string; fields?: string[]; } const workspaceErrorCodes = new Set([ "workspace_invalid", "binding_missing", "workspace_not_activatable", "workspace_stale", "git_unavailable", "git_auth_failed", "git_non_fast_forward", "connector_unavailable", "semantic_index_incompatible", ]); function object(value: unknown): Record | undefined { return value && typeof value === "object" && !Array.isArray(value) ? value as Record : undefined; } function exactObject(value: unknown, keys: readonly string[]): Record | undefined { const source = object(value); return source && Object.keys(source).every((key) => keys.includes(key)) ? source : undefined; } function workspaceRevision(value: unknown, expectedId: string): WorkspaceRevision | undefined { const source = exactObject(value, ["id", "commit", "blob", "snapshotPath"]); if (!source) return undefined; const { id, commit, blob, snapshotPath } = source; if ( id !== expectedId || typeof id !== "string" || !/^[a-z][a-z0-9-]{2,62}$/.test(id) || typeof commit !== "string" || !/^[0-9a-f]{40}$/.test(commit) || typeof blob !== "string" || !/^[0-9a-f]{40}$/.test(blob) || typeof snapshotPath !== "string" || snapshotPath.length === 0 || snapshotPath.trim() !== snapshotPath || /[\x00-]/u.test(snapshotPath) ) return undefined; return { id, commit, blob, snapshotPath }; } function workspaceSummary(value: unknown): WorkspaceSummary | undefined { const source = exactObject(value, [ "id", "name", "file", "displayName", "description", "configurationState", "revision", ]); if (!source) return undefined; const { id, name, file, displayName, description, configurationState } = source; const validText = (candidate: unknown) => typeof candidate === "string" && candidate.length > 0 && candidate.trim() === candidate; if ( typeof id !== "string" || !/^[a-z][a-z0-9-]{2,62}$/.test(id) || name !== id || file !== `${id}/workspace.yaml` || !validText(displayName) || (description !== undefined && !validText(description)) || (configurationState !== "ready" && configurationState !== "configuration_required") ) return undefined; const revision = source.revision === undefined ? undefined : workspaceRevision(source.revision, id); if ( !revision ) return undefined; return { id, name: name as string, file: file as string, displayName: displayName as string, ...(description === undefined ? {} : { description: description as string }), configurationState, revision, }; } function requireWorkspaceRevision(value: unknown, expectedId: string): WorkspaceRevision { const revision = workspaceRevision(value, expectedId); if (!revision) throw new Error("Workspace API returned an invalid workspace revision"); return revision; } /** 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 } : {}) }; } 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 = async (): Promise => { const response = await apiFetch("/workspaces"); if (!Array.isArray(response)) throw new Error("Workspace API returned an invalid workspace summary"); const summaries = response.map(workspaceSummary); if (summaries.some((summary) => !summary)) { throw new Error("Workspace API returned an invalid workspace summary"); } return summaries as WorkspaceSummary[]; }; 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"); const workspace = requireCanonicalWorkspace(source.workspace); return { workspace, revision: requireWorkspaceRevision(source.revision, workspace.workspace.id), }; }; 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" }); function runtimeConfiguration(value: unknown, expectedId: string): WorkspaceRuntimeConfiguration { const source = exactObject(value, [ "workspaceId", "revision", "configurationState", "requirements", ]); if ( !source || source.workspaceId !== expectedId || (source.configurationState !== "ready" && source.configurationState !== "configuration_required") || !Array.isArray(source.requirements) ) throw new Error("Workspace API returned an invalid runtime configuration"); const requirements = source.requirements.map((value) => { const requirement = exactObject(value, [ "id", "connector", "label", "description", "input", "required", "configured", ]); if ( !requirement || typeof requirement.id !== "string" || !/^[a-z0-9][a-z0-9._-]{1,127}$/.test(requirement.id) || (requirement.connector !== "dwh" && requirement.connector !== "evidence") || typeof requirement.label !== "string" || requirement.label.length === 0 || typeof requirement.description !== "string" || requirement.description.length === 0 || (requirement.input !== "password" && requirement.input !== "textarea") || typeof requirement.required !== "boolean" || typeof requirement.configured !== "boolean" ) throw new Error("Workspace API returned an invalid runtime configuration"); return requirement as unknown as WorkspaceSecretRequirement; }); return { workspaceId: expectedId, revision: requireWorkspaceRevision(source.revision, expectedId), configurationState: source.configurationState, requirements, }; } export const getWorkspaceRuntimeConfiguration = async (id: string) => runtimeConfiguration( await apiFetch(`/workspaces/${encodeURIComponent(id)}/runtime-configuration`), id, ); export const saveWorkspaceSecrets = async (id: string, values: Readonly>) => ( runtimeConfiguration(await apiFetch(`/workspaces/${encodeURIComponent(id)}/secrets`, { method: "PUT", body: JSON.stringify({ values }), }), id) ); export const forgetWorkspaceSecret = async (id: string, requirementId: string) => ( runtimeConfiguration(await apiFetch( `/workspaces/${encodeURIComponent(id)}/secrets/${encodeURIComponent(requirementId)}`, { method: "DELETE" }, ), id) );