Initial commit — @crema/integration-registry-client
Typed client + types for the arcadia integration registry (on arcadia-llm-gateway). One createIntegrationsApi(client, mode) factory serves both surfaces: - operator → /api/v1/integrations* (any scope, scope filters) - tenant → /api/v1/me/integrations* (scope forced server-side) Plus shared types (Integration/Credential/UsageEntry/*Input/TestVerdict) and display helpers (formatUsd, credentialHealth). Consumed by arcadia-admin and arcadia-console as a sibling lib via vite/tsconfig aliases (no build step). Secrets are write-only; the rich panels stay per-app. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
.DS_Store
|
||||
26
README.md
Normal file
26
README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# @crema/integration-registry-client
|
||||
|
||||
Typed client + types for the **arcadia integration registry** — the external-API
|
||||
credential plane hosted on `arcadia-llm-gateway`. Consumed by `arcadia-admin`
|
||||
(operator) and `arcadia-console` (tenant) as a sibling lib via vite/tsconfig
|
||||
aliases (no build step — the app compiles `src/` directly).
|
||||
|
||||
```ts
|
||||
import { createIntegrationsApi } from "@crema/integration-registry-client"
|
||||
|
||||
// gateway-pointed ArcadiaClient (see each app's app/lib/gateway.ts)
|
||||
const api = createIntegrationsApi(gatewayClient, "tenant") // or "operator"
|
||||
|
||||
await api.list()
|
||||
await api.create({ provider: "tavily", capability: "web_search" })
|
||||
await api.test(id) // throws ArcadiaError on 409 expired / 429 over-budget
|
||||
```
|
||||
|
||||
- `operator` mode → `/api/v1/integrations*` (any scope, scope filters honoured).
|
||||
- `tenant` mode → `/api/v1/me/integrations*` (scope forced to the caller server-side).
|
||||
|
||||
Secrets are **write-only**: reads carry `has_secret`, never a value.
|
||||
|
||||
The rich management **page/components stay per-app** (they use each app's shadcn
|
||||
primitives and the operator/tenant surfaces legitimately diverge) — this lib owns
|
||||
the client, types, and display helpers only.
|
||||
14
package.json
Normal file
14
package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "@crema/integration-registry-client",
|
||||
"version": "0.1.0",
|
||||
"description": "Typed client + types for the arcadia integration registry (on arcadia-llm-gateway). One factory serves the operator and tenant surfaces.",
|
||||
"type": "module",
|
||||
"main": "src/index.tsx",
|
||||
"types": "src/index.tsx",
|
||||
"exports": {
|
||||
".": "./src/index.tsx"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@crema/arcadia-client": "*"
|
||||
}
|
||||
}
|
||||
198
src/index.tsx
Normal file
198
src/index.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
"use client"
|
||||
|
||||
// PURPOSE: Typed client + types for the arcadia integration registry (hosted
|
||||
// on arcadia-llm-gateway). One factory serves both the operator surface
|
||||
// (/api/v1/integrations*) and the tenant surface (/api/v1/me/integrations*),
|
||||
// so arcadia-admin and arcadia-console share a single contract. Secrets are
|
||||
// write-only end to end: reads carry `has_secret`, never a value.
|
||||
//
|
||||
// EXPORTS: types (Integration, Credential, UsageEntry, *Input, TestVerdict, …),
|
||||
// createIntegrationsApi(client, mode), formatUsd, credentialHealth.
|
||||
|
||||
import type { ArcadiaClient } from "@crema/arcadia-client"
|
||||
|
||||
export type IntegrationsMode = "operator" | "tenant"
|
||||
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<string, unknown>
|
||||
/** 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 {
|
||||
/** Operator-only — the tenant surface forces scope=tenant + the caller's id. */
|
||||
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<string, unknown>
|
||||
}
|
||||
|
||||
/** Verdict from the "Test" gate-check (resolve + policy, no live upstream). */
|
||||
export interface TestVerdict {
|
||||
status: string
|
||||
auth_kind?: AuthKind
|
||||
policy?: {
|
||||
within_budget: boolean
|
||||
within_rate: boolean
|
||||
remaining_budget_usd: string | null
|
||||
}
|
||||
}
|
||||
|
||||
/** Operator-only list/usage filter; ignored on the tenant surface. */
|
||||
export interface ScopeFilter {
|
||||
scope?: Scope
|
||||
scope_id?: string
|
||||
}
|
||||
|
||||
export interface IntegrationsApi {
|
||||
list(filter?: ScopeFilter): Promise<Integration[]>
|
||||
create(input: IntegrationInput): Promise<Integration>
|
||||
update(id: string, input: Partial<IntegrationInput>): Promise<Integration>
|
||||
remove(id: string): Promise<void>
|
||||
addCredential(integrationId: string, input: CredentialInput): Promise<Credential>
|
||||
updateCredential(credentialId: string, input: Partial<CredentialInput>): Promise<Credential>
|
||||
deleteCredential(credentialId: string): Promise<void>
|
||||
/** Runs the registry's policy gate; throws ArcadiaError on 409/429/404. */
|
||||
test(id: string): Promise<TestVerdict>
|
||||
usage(filter?: ScopeFilter): Promise<UsageEntry[]>
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a registry client over a gateway-pointed `ArcadiaClient`.
|
||||
*
|
||||
* `mode` selects the surface: `operator` → `/api/v1/integrations*` (any scope,
|
||||
* scope filters honoured); `tenant` → `/api/v1/me/integrations*` (scope forced
|
||||
* to the caller's tenant server-side, filters ignored).
|
||||
*/
|
||||
export function createIntegrationsApi(c: ArcadiaClient, mode: IntegrationsMode): IntegrationsApi {
|
||||
const base = mode === "operator" ? "/api/v1/integrations" : "/api/v1/me/integrations"
|
||||
const credBase = mode === "operator" ? "/api/v1/credentials" : "/api/v1/me/credentials"
|
||||
// Scope filters only mean something on the operator surface.
|
||||
const params = (f?: ScopeFilter) =>
|
||||
mode === "operator" && f ? { scope: f.scope, scope_id: f.scope_id } : undefined
|
||||
|
||||
return {
|
||||
async list(filter) {
|
||||
const r = await c.GET<{ integrations: Integration[] }>(base, { params: params(filter) })
|
||||
return r.integrations
|
||||
},
|
||||
async create(input) {
|
||||
const r = await c.POST<{ integration: Integration }>(base, { body: input })
|
||||
return r.integration
|
||||
},
|
||||
async update(id, input) {
|
||||
const r = await c.PATCH<{ integration: Integration }>(`${base}/${id}`, { body: input })
|
||||
return r.integration
|
||||
},
|
||||
async remove(id) {
|
||||
await c.DELETE(`${base}/${id}`)
|
||||
},
|
||||
async addCredential(integrationId, input) {
|
||||
const r = await c.POST<{ credential: Credential }>(`${base}/${integrationId}/credentials`, {
|
||||
body: input,
|
||||
})
|
||||
return r.credential
|
||||
},
|
||||
async updateCredential(credentialId, input) {
|
||||
const r = await c.PATCH<{ credential: Credential }>(`${credBase}/${credentialId}`, {
|
||||
body: input,
|
||||
})
|
||||
return r.credential
|
||||
},
|
||||
async deleteCredential(credentialId) {
|
||||
await c.DELETE(`${credBase}/${credentialId}`)
|
||||
},
|
||||
async test(id) {
|
||||
return c.POST<TestVerdict>(`${base}/${id}/test`)
|
||||
},
|
||||
async usage(filter) {
|
||||
const r = await c.GET<{ usage: UsageEntry[] }>(`${base}/usage`, { params: params(filter) })
|
||||
return r.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"
|
||||
}
|
||||
Reference in New Issue
Block a user