// Integration-registry API client (operator surface). // // Talks to arcadia-llm-gateway's `/api/v1/integrations*` endpoints — the // platform operator manages pooled/platform credentials at ANY scope and // inspects cross-tenant usage *metadata* (never secrets; reads return // `has_secret`, never a value). // // The tenant mirror (`/api/v1/me/integrations*`, scoped to the caller) lives // in arcadia-console. Pure functions over an injected gateway `ArcadiaClient` // (see `~/lib/gateway`); no app coupling, so this lifts into a shared lib once // both apps exist. import type { ArcadiaClient } from "@crema/arcadia-client" export type Scope = "platform" | "tenant" | "app" | "user" | "agent" export type AuthKind = "bearer_static" | "basic" | "api_key_header" | "oauth2" export type CredentialSource = "byo" | "pooled" export type CredentialStatus = "active" | "expired" | "revoked" export interface CostModel { unit?: "call" | "search" | "1k_tokens" price_usd?: string | number currency?: string } export interface Constraints { rate_per_min?: number monthly_budget_usd?: string | number monthly_call_cap?: number } export interface Credential { id: string integration_id: string secret_name: string auth_kind: AuthKind meta: Record /** Presence, not value — the secret is write-only. */ has_secret: boolean expires_at: string | null last_rotated_at: string | null source: CredentialSource status: CredentialStatus } export interface Integration { id: string scope: Scope scope_id: string | null provider: string capability: string | null display_name: string | null description: string | null docs_url: string | null base_url: string | null enabled: boolean cost_model: CostModel constraints: Constraints created_by: string | null credentials: Credential[] } export interface UsageEntry { integration_id: string scope: Scope scope_id: string | null provider: string capability: string | null units: number cost_usd: string calls: number } export interface IntegrationInput { scope?: Scope scope_id?: string | null provider: string capability?: string | null display_name?: string | null description?: string | null docs_url?: string | null base_url?: string | null enabled?: boolean cost_model?: CostModel constraints?: Constraints } export interface CredentialInput { secret_name: string auth_kind: AuthKind /** Plaintext, write-only — sent on create/rotate, never returned. */ secret?: string source?: CredentialSource expires_at?: string | null meta?: Record } export interface TestVerdict { status: string auth_kind?: AuthKind policy?: { within_budget: boolean within_rate: boolean remaining_budget_usd: string | null } } export interface ScopeFilter { scope?: Scope scope_id?: string } const BASE = "/api/v1/integrations" export async function listIntegrations( c: ArcadiaClient, filter: ScopeFilter = {}, ): Promise { const res = await c.GET<{ integrations: Integration[] }>(BASE, { params: { scope: filter.scope, scope_id: filter.scope_id }, }) return res.integrations } export async function createIntegration( c: ArcadiaClient, input: IntegrationInput, ): Promise { const res = await c.POST<{ integration: Integration }>(BASE, { body: input }) return res.integration } export async function updateIntegration( c: ArcadiaClient, id: string, input: Partial, ): Promise { const res = await c.PATCH<{ integration: Integration }>(`${BASE}/${id}`, { body: input }) return res.integration } export async function deleteIntegration(c: ArcadiaClient, id: string): Promise { await c.DELETE(`${BASE}/${id}`) } export async function addCredential( c: ArcadiaClient, integrationId: string, input: CredentialInput, ): Promise { const res = await c.POST<{ credential: Credential }>(`${BASE}/${integrationId}/credentials`, { body: input, }) return res.credential } export async function updateCredential( c: ArcadiaClient, credentialId: string, input: Partial, ): Promise { const res = await c.PATCH<{ credential: Credential }>(`/api/v1/credentials/${credentialId}`, { body: input, }) return res.credential } export async function deleteCredential(c: ArcadiaClient, credentialId: string): Promise { await c.DELETE(`/api/v1/credentials/${credentialId}`) } /** * Run the registry's policy gate against an integration's credential. 200 with * a verdict; 4xx (409 expired / 429 over-budget / 404 no credential) is thrown * by `ArcadiaClient` as an `ArcadiaError` — inspect `err.status`. */ export async function testIntegration(c: ArcadiaClient, id: string): Promise { return c.POST(`${BASE}/${id}/test`) } export async function usageSummary( c: ArcadiaClient, filter: ScopeFilter = {}, ): Promise { const res = await c.GET<{ usage: UsageEntry[] }>(`${BASE}/usage`, { params: { scope: filter.scope, scope_id: filter.scope_id }, }) return res.usage } // ── display helpers ──────────────────────────────────────────────────── export function formatUsd(value: string | number | null | undefined): string { if (value === null || value === undefined) return "—" const n = typeof value === "string" ? Number(value) : value if (Number.isNaN(n)) return "—" return `$${n.toFixed(n < 0.01 && n > 0 ? 4 : 2)}` } export function credentialHealth(cred: Credential): "ok" | "expired" | "revoked" | "missing" { if (cred.status === "revoked") return "revoked" if (!cred.has_secret) return "missing" if (cred.expires_at && new Date(cred.expires_at).getTime() < Date.now()) return "expired" return "ok" }