export class ApiError extends Error { status: number; code: string; correlationId?: string; payload: unknown; constructor(status: number, code: string, message: string, correlationId?: string, payload?: unknown) { super(message); this.status = status; this.code = code; this.correlationId = correlationId; this.payload = payload; } } export const UNAUTH_EVENT = "akos:unauthenticated"; type Opts = { method?: string; body?: unknown; raw?: boolean; signal?: AbortSignal; noRedirect?: boolean }; export async function api(path: string, opts: Opts = {}): Promise { const url = path.startsWith("/") ? path : `/api/v1/${path}`; const init: RequestInit = { method: opts.method ?? (opts.body !== undefined ? "POST" : "GET"), credentials: "include", headers: {}, signal: opts.signal }; if (opts.body !== undefined) { (init.headers as Record)["Content-Type"] = "application/json"; init.body = JSON.stringify(opts.body); } const r = await fetch(url, init); if (r.status === 401 && !opts.noRedirect) { window.dispatchEvent(new CustomEvent(UNAUTH_EVENT)); } if (opts.raw) { if (!r.ok) { let msg = r.statusText; try { const j = await r.json(); msg = j?.error?.message ?? msg; } catch { /* ignore */ } throw new ApiError(r.status, "http_error", msg); } return (await r.text()) as unknown as T; } const ct = r.headers.get("content-type") ?? ""; const data = ct.includes("application/json") ? await r.json() : await r.text(); if (!r.ok) { const e = (data && typeof data === "object" && "error" in data ? (data as { error: { code?: string; message?: string; correlation_id?: string } }).error : undefined) ?? {}; throw new ApiError(r.status, e.code ?? "http_error", e.message ?? (typeof data === "string" ? data.slice(0, 200) : r.statusText), e.correlation_id, e); } return data as T; } export const v1 = (p: string) => `/api/v1/${p.replace(/^\//, "")}`; export function qs(params: Record): string { const u = new URLSearchParams(); for (const [k, v] of Object.entries(params)) if (v !== undefined && v !== null && v !== "") u.set(k, String(v)); const s = u.toString(); return s ? `?${s}` : ""; } // ---------- shared types (shape mirrors akos/api/*.py) ---------- export type Me = { id: string; email: string; role: "admin" | "operator" | "viewer"; kind: string; scopes: string[]; project_id: string | null }; export type Job = { id: string; kind: string; state: string; attempts: number; max_attempts?: number; error: string | null; result: Record | null; created_at: string; finished_at: string | null; started_at?: string | null; locked_by?: string | null; created_by?: string | null }; export type Overview = { counts: Record; lifecycle: { lifecycle: string; n: number }[]; knowledge_states: { knowledge_state: string; n: number }[]; execution_states_7d: { state: string; n: number }[]; daily: { day: string; n: number; ok: number; bad: number }[]; recent_executions: ExecutionRow[]; debt: { severity: string; n: number }[]; jobs: { by_state: Record; queued: number; running: number; dead: number; oldest_queued: string | null }; workers: Worker[]; version: string; }; export type Worker = { name: string; pid: number | null; host: string | null; version: string | null; started_at: string; last_seen: string; current_job: string | null }; export type ApiRow = { id: string; slug: string; name: string; kind: string; base_url: string | null; lifecycle: string; knowledge_state: string; opaque_internal_implementation: boolean; tags: string[]; updated_at: string; created_at: string; provider_slug: string; provider_name: string; versions: number; endpoints: number; packages: number; open_debt: number; executions: number }; export type Endpoint = { id: string; operation_id: string; method: string; path: string; summary: string | null; description?: string | null; inferred: boolean; pagination: Record; parameters: Param[]; request_schema?: unknown; response_schema?: unknown; rate_limit?: Record }; export type Param = { name: string; in: string; required: boolean; description?: string; schema?: Record; example?: unknown }; export type Variable = { id: string; name: string; json_path: string | null; data_type: string | null; unit: string | null; description: string | null; semantic_concept: string | null; endpoint_id: string | null }; export type Evidence = { id: string; subject_type: string; subject_id: string; kind: string; result: "pass" | "fail" | "blocked" | "info"; command: string | null; output_excerpt: string | null; output_hash: string | null; artifact_id: string | null; details: Record; created_by: string | null; created_at: string }; export type Debt = { id: string; kind: string; severity: "low" | "medium" | "high" | "critical"; subject_type: string; subject_id: string; project_ids: string[]; description: string; remediation: string | null; owner_id: string | null; status: string; evidence_id: string | null; created_at: string; resolved_at: string | null; owner?: string | null; subject_label?: string | null }; export type DriftAlert = { id: string; api_version_id: string; endpoint_id: string | null; kind: string; details: Record; affected_projects: string[]; status: string; debt_id: string | null; detected_at: string; resolved_at: string | null; version?: string; api_name?: string; api_id?: string }; export type VersionRow = { id: string; version: string; lifecycle: string; contract_hash: string; source_kind: string; source_url: string | null; created_at: string; created_by: string | null; endpoints: number }; export type PackageRow = { id: string; akp_id: string; version: number; lifecycle: string; knowledge_state: string; hash: string; created_at: string; superseded_by: string | null; credential_ref_id?: string | null; api_name?: string; api_id?: string; opaque?: boolean; provider_slug?: string; open_debt?: number; evidence_pass?: number; evidence_fail?: number; license?: Record }; export type ApiDetail = ApiRow & { description: string | null; docs_url: string | null; auth_scheme: Record; current_version_id: string | null; provider_id: string; provider_license: Record; provider_docs: string | null; versions: VersionRow[]; endpoints?: Endpoint[]; variables?: Variable[]; packages: PackageRow[]; evidence: Evidence[]; debt: Debt[]; executions: ExecutionRow[]; drift: DriftAlert[]; allowed_transitions: string[] }; export type PackageDetail = PackageRow & { api_version_id: string; created_by: string | null; manifest: Record; contract_yaml: string; recipe: Record; readme: string; license: Record; api_slug: string; api_kind: string; base_url: string | null; provider_name: string; api_version_label: string; contract_hash: string; files: { filename: string; language: string; static_scan: Record; sha256: string; size_bytes: number }[]; evidence: Evidence[]; debt: Debt[]; endpoints: Endpoint[]; allowed_transitions: string[] }; export type ExecutionRow = { id: string; state: string; akp_lifecycle?: string | null; method: string | null; url: string | null; status_code: number | null; duration_ms: number | null; created_at: string; finished_at?: string | null; policy_mode?: string; purpose?: string | null; caller?: string | null; observed?: boolean; provisional?: boolean; response_hash?: string | null; response_bytes?: number | null; error?: string | null; correlation_id?: string | null; project_slug?: string; api_name?: string | null; api_id?: string | null; akp_id?: string | null; akp_version?: number | null; agent_name?: string | null; decision?: string | null }; export type ExecutionDetail = ExecutionRow & { project_id: string; params: Record; request_fingerprint: string | null; credential_name: string | null; credential_ref: string | null; credential_version: number | null; operation_id: string | null; endpoint_path: string | null; endpoint_id: string | null; package_id: string | null; api_version_id: string | null; response_media_type: string | null; response_artifact_id: string | null; retries: number; started_at: string | null; api_opaque: boolean | null; policy_decision: { decision: string; reasons: string[]; checks: Record; mode: string; created_at: string } | null; artifact?: { id: string; sha256: string; size_bytes: number; media_type: string; retention: string } | null; lineage: { from_type: string; from_id: string; to_type: string; to_id: string; relation: string }[]; audit: { seq: number; ts: string; actor_type: string; actor_id: string | null; action: string; details: Record; hash: string }[]; egress: { ts: string; host: string; port: number; decision: string; reason: string | null }[] }; export type Credential = { id: string; name: string; ref: string; scheme: string; placement: string | null; scopes: string[]; allowed_projects: string[]; environment: string; version: number; fingerprint: string | null; expires_at: string | null; rotated_at: string | null; revoked_at: string | null; created_at: string; provider_name?: string | null; provider_slug?: string | null; owner?: string | null; uses?: number; last_used?: string | null; vault_present?: boolean; packages?: PackageRow[] }; export type Project = { id: string; slug: string; name: string; policy_mode: string; owner_id: string | null; approved_akps: string[]; budget: Record; settings: Record; created_at: string; executions?: number; datasets?: number; agents?: number; executions_24h?: number }; export type Agent = { id: string; name: string; kind: string; project_id: string | null; budget: Record; permissions: Record; created_at: string; last_seen_at: string | null; project_slug?: string; executions?: number; executions_24h?: number }; export type Dataset = { id: string; name: string; row_count: number | null; verification: string; granularity: string | null; units: Record; identifiers: { keys?: string[] }; hash: string | null; created_at: string; created_by: string | null; license: Record; project_slug: string; project_id?: string; executions?: number; transformations?: number }; export type DatasetDetail = Dataset & { project_id: string; execution_ids: string[]; artifact_id: string | null; schema_json: { columns?: Record; records_path?: string | null }; coverage: Record; assessment: { verified: boolean; checks: { check: string; ok: boolean; detail: string }[] }; executions: ExecutionRow[]; transformations: { id: string; name: string; kind: string; code_hash: string; input_dataset_ids: string[]; params: Record; validation: Record; created_at: string }[]; exports: { artifact_id: string; details: Record; created_at: string; sha256: string; size_bytes: number }[]; preview: Record[] }; export type Graph = { nodes: GraphNode[]; edges: { from: string; to: string; relation: string }[] }; export type GraphNode = { id: string; type: string; label: string; [k: string]: unknown }; export type Recipe = { id: string; name: string; version: number; status: string; validation: Record; output_dataset_id: string | null; created_at: string; spec: Record; project_slug?: string; project_id: string }; export type AuditEvent = { seq: number; id: string; ts: string; actor_type: string; actor_id: string | null; action: string; target_type: string | null; target_id: string | null; project_id: string | null; correlation_id: string | null; details: Record; prev_hash: string | null; hash: string }; export type SearchResult = { type: string; id: string; title: string; snippet: string; lifecycle: string; knowledge_state: string; provider: string; api_id: string; rank: number }; export type Health = { status: string; version: string; uptime_s: number; db: { ok: boolean; latency_ms: number; size_bytes: number; migrations: { name: string; applied_at: string }[] }; workers: Worker[]; live_workers: number; proxy: { port: number; listening: boolean }; jobs: Overview["jobs"]; sandbox: boolean; disk: { free_bytes: number; total_bytes: number } | null; data_dir: string; public_url: string; host: string }; export type Metrics = { executions_24h: { p50: number | null; p95: number | null; max: number | null; n: number }; jobs_24h: { done: number; dead: number; avg_s: number | null }; egress_24h: { decision: string; n: number }[]; by_api_7d: { name: string; n: number; avg_ms: number | null; ok: number }[]; hourly: { hour: string; n: number }[] };