// Tenant Registry client — covers everything the portal needs to call // from server components and server actions. export type Tenant = { id: string; slug: string; name: string; status: "active" | "trial" | "frozen" | "archived" | "demo"; kind: "customer" | "demo"; plan: "starter" | "professional" | "enterprise"; trial_ends_at?: string | null; created_at: string; updated_at: string; }; export type CatalogEntry = { key: string; name: string; description: string; plans_required: string[]; supports_trial: boolean; demo_url?: string; }; export type Entitlement = { tenant_id: string; product: string; enabled: boolean; config: Record; expires_at?: string | null; }; export type APIKey = { id: string; tenant_id: string; product?: string; name: string; scopes: string[]; prefix: string; created_by?: string; last_used_at?: string | null; revoked_at?: string | null; created_at: string; }; export type AuditEvent = { id: number; tenant_id?: string; actor_id?: string; actor_name?: string; actor_type?: string; action: string; target_id?: string; target_type?: string; target_name?: string; product?: string; metadata?: Record; source_ip?: string; user_agent?: string; created_at: string; }; export type AuditPage = { items: AuditEvent[]; next_cursor?: number; }; function baseUrl(): string { return process.env.TENANT_REGISTRY_URL ?? "http://localhost:8090"; } async function req( method: string, path: string, body?: unknown, ): Promise<{ status: number; data: T | null }> { const init: RequestInit = { method, headers: { accept: "application/json" }, cache: "no-store", }; if (body !== undefined) { init.body = JSON.stringify(body); init.headers = { ...init.headers, "content-type": "application/json" }; } const res = await fetch(`${baseUrl()}${path}`, init); if (res.status === 204) return { status: 204, data: null }; const data = (await res.json().catch(() => null)) as T | null; return { status: res.status, data }; } // ─── reads ─────────────────────────────────────────────────────────────── export async function fetchTenantBySlug(slug: string): Promise { const { status, data } = await req( "GET", `/v1/tenants/by-slug/${encodeURIComponent(slug)}`, ); if (status === 404) return null; if (status >= 400 || !data) { throw new Error(`tenant-registry: GET tenant ${status}`); } return data; } export async function fetchCatalog(): Promise { const { status, data } = await req<{ items: CatalogEntry[] }>( "GET", "/v1/catalog", ); if (status !== 200 || !data) { throw new Error(`tenant-registry: GET catalog ${status}`); } return data.items; } export async function fetchEntitlements(tenantId: string): Promise { const { status, data } = await req<{ items: Entitlement[] }>( "GET", `/v1/entitlements?tenant_id=${encodeURIComponent(tenantId)}`, ); if (status === 404) return []; if (status !== 200 || !data) { throw new Error(`tenant-registry: GET entitlements ${status}`); } return data.items; } // ─── mutations ─────────────────────────────────────────────────────────── export type RequestProductResult = | { ok: true } | { ok: false; error: string }; export async function requestProduct( tenantId: string, product: string, ): Promise { const { status } = await req("POST", "/v1/catalog/request", { tenant_id: tenantId, product, }); if (status === 202) return { ok: true }; if (status === 404) return { ok: false, error: "tenant_not_found" }; if (status === 400) return { ok: false, error: "invalid_input" }; return { ok: false, error: `unexpected_${status}` }; } export type StartTrialResult = | { ok: true; entitlement: Entitlement } | { ok: false; error: string }; export async function startTrial( tenantId: string, product: string, ): Promise { const { status, data } = await req( "POST", "/v1/catalog/trial-request", { tenant_id: tenantId, product }, ); if (status === 201 && data) return { ok: true, entitlement: data }; if (status === 404) return { ok: false, error: "tenant_not_found" }; if (status === 400) return { ok: false, error: "invalid_input" }; return { ok: false, error: `unexpected_${status}` }; } export type CreateTenantInput = { slug: string; name: string; plan?: "starter" | "professional" | "enterprise"; admin_email?: string; admin_name?: string; }; export type CreateTenantResult = | { ok: true; tenant: Tenant; invite_url?: string } | { ok: false; error: string }; export async function createTenant( in_: CreateTenantInput, ): Promise { const { status, data } = await req<{ tenant: Tenant; invite_url?: string }>( "POST", "/v1/tenants", in_, ); if (status === 201 && data) { return { ok: true, tenant: data.tenant, invite_url: data.invite_url }; } if (status === 409) return { ok: false, error: "slug_taken" }; if (status === 400) return { ok: false, error: "invalid_input" }; return { ok: false, error: `unexpected_${status}` }; } // ─── api keys ──────────────────────────────────────────────────────────── export async function fetchAPIKeys(tenantId: string): Promise { const { status, data } = await req<{ items: APIKey[] }>( "GET", `/v1/api-keys?tenant_id=${encodeURIComponent(tenantId)}`, ); if (status === 404) return []; if (status !== 200 || !data) { throw new Error(`tenant-registry: GET api-keys ${status}`); } return data.items; } export type CreateAPIKeyInput = { tenant_id: string; name: string; product?: string; scopes?: string[]; created_by?: string; }; export type CreateAPIKeyResult = | { ok: true; api_key: APIKey; plaintext: string } | { ok: false; error: string }; export async function createAPIKey(in_: CreateAPIKeyInput): Promise { const { status, data } = await req<{ api_key: APIKey; plaintext: string }>( "POST", "/v1/api-keys", in_, ); if (status === 201 && data) return { ok: true, api_key: data.api_key, plaintext: data.plaintext }; if (status === 404) return { ok: false, error: "tenant_not_found" }; if (status === 400) return { ok: false, error: "invalid_input" }; if (status === 409) return { ok: false, error: "name_taken" }; return { ok: false, error: `unexpected_${status}` }; } export async function revokeAPIKey(id: string): Promise<{ ok: boolean; error?: string }> { const { status } = await req("DELETE", `/v1/api-keys/${encodeURIComponent(id)}`); if (status === 204) return { ok: true }; if (status === 404) return { ok: false, error: "not_found" }; return { ok: false, error: `unexpected_${status}` }; } // ─── audit ─────────────────────────────────────────────────────────────── export type AuditFilter = { tenant_id?: string; product?: string; actor_id?: string; action?: string; since?: string; until?: string; limit?: number; cursor?: number; }; export async function fetchAudit(f: AuditFilter): Promise { const qs = new URLSearchParams(); if (f.tenant_id) qs.set("tenant_id", f.tenant_id); if (f.product) qs.set("product", f.product); if (f.actor_id) qs.set("actor_id", f.actor_id); if (f.action) qs.set("action", f.action); if (f.since) qs.set("since", f.since); if (f.until) qs.set("until", f.until); if (f.limit) qs.set("limit", String(f.limit)); if (f.cursor) qs.set("cursor", String(f.cursor)); const { status, data } = await req("GET", `/v1/audit?${qs.toString()}`); if (status !== 200 || !data) { throw new Error(`tenant-registry: GET audit ${status}`); } return data; }