1import "server-only";23/**4 * Server-side client for the Fetcha API service (Fastify). Used by server actions and route5 * handlers only; authenticated with the shared internal service token.6 */7const API_URL = (process.env.API_URL ?? "http://127.0.0.1:8221").replace(/\/$/, "");8const TOKEN = process.env.INTERNAL_SERVICE_TOKEN ?? "";910export interface ApiErrorShape {11 error: { code: string; message: string; request_id: string | null; details?: Record<string, unknown> };12}1314export class InternalApiError extends Error {15 constructor(16 readonly status: number,17 readonly code: string,18 message: string,19 readonly requestId: string | null,20 readonly details?: Record<string, unknown>,21 ) {22 super(message);23 this.name = "InternalApiError";24 }25}2627async function call<T>(path: string, init: RequestInit & { timeoutMs?: number } = {}): Promise<T> {28 const ac = new AbortController();29 const t = setTimeout(() => ac.abort(), init.timeoutMs ?? 150_000);30 try {31 const res = await fetch(`${API_URL}${path}`, {32 ...init,33 headers: { authorization: `Bearer ${TOKEN}`, "content-type": "application/json", ...(init.headers ?? {}) },34 signal: ac.signal,35 cache: "no-store",36 });37 const text = await res.text();38 let data: unknown = null;39 try {40 data = text ? JSON.parse(text) : null;41 } catch {42 data = null;43 }44 if (!res.ok) {45 const err = (data as ApiErrorShape | null)?.error;46 throw new InternalApiError(res.status, err?.code ?? "INTERNAL_ERROR", err?.message ?? `API error ${res.status}`, err?.request_id ?? res.headers.get("x-fetcha-request-id"), err?.details);47 }48 return data as T;49 } catch (e) {50 if (e instanceof InternalApiError) throw e;51 if ((e as Error).name === "AbortError") throw new InternalApiError(504, "TARGET_TIMEOUT", "The request timed out.", null);52 throw new InternalApiError(503, "PROVIDER_UNAVAILABLE", "The Fetcha API service is unreachable.", null);53 } finally {54 clearTimeout(t);55 }56}5758export const internalApi = {59 health: () => call<{ status: string; version: string }>("/health", { timeoutMs: 5000 }),60 ready: () => call<{ status: string; checks: Record<string, boolean>; available_networks: string[] }>("/ready", { timeoutMs: 8000 }).catch((e) => (e instanceof InternalApiError && e.status === 503 ? { status: "degraded", checks: {}, available_networks: [] } : Promise.reject(e))),61 playgroundFetch: (projectId: string, userId: string, request: unknown) =>62 call<unknown>("/internal/fetch", { method: "POST", body: JSON.stringify({ project_id: projectId, user_id: userId, request }) }),63 createSession: (projectId: string, userId: string, options: unknown) =>64 call<unknown>("/internal/sessions", { method: "POST", body: JSON.stringify({ project_id: projectId, user_id: userId, options }) }),65 listSessions: (projectId: string, userId: string) => call<{ data: unknown[] }>(`/internal/sessions?project_id=${projectId}&user_id=${userId}`),66 closeSession: (projectId: string, userId: string, id: string) => call<unknown>(`/internal/sessions/${id}?project_id=${projectId}&user_id=${userId}`, { method: "DELETE" }),67 usage: (projectId: string, userId: string) => call<Record<string, unknown>>(`/internal/usage?project_id=${projectId}&user_id=${userId}`),68 providers: () =>69 call<{70 providers: Array<{71 id: string;72 label: string;73 configured: boolean;74 networks: string[];75 prices: Record<string, number>;76 health: Array<{ network: string; status: string; latency_ms: number | null; detail: string | null; checked_at: string }>;77 circuits: Array<{ key: string; state: string; failures: number; successes: number; openedAt: number | null }>;78 }>;79 available_networks: string[];80 }>("/internal/providers", { timeoutMs: 10_000 }),81 probeProviders: () => call<{ ok: boolean }>("/internal/providers/probe", { method: "POST", timeoutMs: 60_000 }),82 reloadProviders: () => call<{ ok: boolean }>("/internal/providers/reload", { method: "POST" }),83 resetCircuit: (key?: string) => call<{ ok: boolean }>("/internal/circuits/reset", { method: "POST", body: JSON.stringify({ key }) }),84 invalidateKey: (keyHash: string) => call<{ ok: boolean }>("/internal/keys/invalidate", { method: "POST", body: JSON.stringify({ key_hash: keyHash }) }).catch(() => ({ ok: false })),85};86