/** * Oracle API Client — production-only client for the Oracle v1 backend. */ import type { CanvasPage, PromptSubmitRequest, PromptSubmitResponse, ForkCreateRequest, ForkCreateResponse, MergeRequestCreateRequest, MergeRequest, MergeReviewRequest, ComponentTemplate, UserProfile, OracleWSMessage, OracleEnvelope, CanvasPageRevision, } from '../types/canvas'; import { VELOCITY_TOKEN_KEY } from '@/lib/velocityPlatformClient'; function getBrowserOrigin(): string { return typeof window !== 'undefined' ? window.location.origin : ''; } function resolveBaseUrl(): string { const configured = (import.meta.env.VITE_ORACLE_API_URL as string | undefined)?.trim(); if (configured) { return configured.replace(/\/$/, ''); } return getBrowserOrigin(); } function resolveWsUrl(): string { const configured = (import.meta.env.VITE_ORACLE_WS_URL as string | undefined)?.trim(); if (configured) { return configured.replace(/\/$/, ''); } const origin = getBrowserOrigin(); return origin ? origin.replace(/^http/, 'ws') : ''; } const BASE_URL = resolveBaseUrl(); const WS_URL = resolveWsUrl(); function apiUrl(path: string): string { return `${BASE_URL}/api/oracle/v1${path}`; } async function apiFetch( path: string, options?: RequestInit & { idempotencyKey?: string }, ): Promise { const headers: Record = { 'Content-Type': 'application/json', 'X-Oracle-Contract-Version': 'v1', ...(options?.idempotencyKey ? { 'Idempotency-Key': options.idempotencyKey } : {}), }; const token = localStorage.getItem('oracle_jwt') ?? localStorage.getItem(VELOCITY_TOKEN_KEY); if (token) headers.Authorization = `Bearer ${token}`; const res = await fetch(apiUrl(path), { ...options, headers }); if (!res.ok) { const body = await res.json().catch(() => ({})); throw Object.assign(new Error(body?.error?.message ?? body?.detail?.errors?.join?.(', ') ?? `HTTP ${res.status}`), { apiError: body }); } const body = await res.json() as OracleEnvelope | T; if (typeof body === 'object' && body !== null && 'data' in body) { return (body as OracleEnvelope).data; } return body as T; } export async function fetchMe(): Promise { return apiFetch('/me'); } export async function fetchCanvasPage(pageId: string): Promise { return apiFetch(`/canvas-pages/${pageId}`); } export async function submitPrompt( pageId: string, payload: PromptSubmitRequest, ): Promise { return apiFetch(`/canvas-pages/${pageId}/prompts`, { method: 'POST', body: JSON.stringify(payload), idempotencyKey: payload.clientRequestId, }); } export async function rollbackPage( pageId: string, targetRevision: number, clientRequestId: string, ): Promise<{ headRevision: number; pageId: string; components: CanvasPage['components'] }> { return apiFetch<{ headRevision: number; pageId: string; components: CanvasPage['components'] }>( `/canvas-pages/${pageId}/rollback`, { method: 'POST', body: JSON.stringify({ targetRevision, clientRequestId }), idempotencyKey: clientRequestId, }, ); } export async function listRevisions(pageId: string): Promise { return apiFetch(`/canvas-pages/${pageId}/revisions`); } export async function createFork( pageId: string, payload: ForkCreateRequest, ): Promise { return apiFetch(`/canvas-pages/${pageId}/forks`, { method: 'POST', body: JSON.stringify(payload), }); } export async function openMergeRequest( payload: MergeRequestCreateRequest, ): Promise { return apiFetch('/merge-requests', { method: 'POST', body: JSON.stringify(payload), }); } export async function reviewMergeRequest( mrId: string, payload: MergeReviewRequest, ): Promise { return apiFetch(`/merge-requests/${mrId}/review`, { method: 'POST', body: JSON.stringify(payload), }); } export async function listMergeRequests(pageId: string): Promise { return apiFetch(`/merge-requests?targetPageId=${pageId}`); } export async function listComponentTemplates(filters?: { category?: string; status?: string; }): Promise { const qs = new URLSearchParams(filters as Record).toString(); return apiFetch(`/component-templates${qs ? `?${qs}` : ''}`); } export async function synthesizeTemplate(params: { prompt: string; dataShape: string[]; styleSignatureRef?: string; }): Promise { return apiFetch('/component-templates/synthesize', { method: 'POST', body: JSON.stringify(params), }); } export function connectPageSocket( pageId: string, handlers: { onMessage: (msg: OracleWSMessage) => void; onReconnect?: () => void; onOpen?: () => void; onClose: () => void; }, ): () => void { if (!WS_URL && !BASE_URL) { handlers.onClose(); return () => undefined; } const wsBase = WS_URL || BASE_URL.replace(/^http/, 'ws'); let ws: WebSocket; let stopped = false; let retryTimeout: ReturnType | undefined; function connect() { ws = new WebSocket(`${wsBase}/ws/oracle/canvas/${pageId}`); ws.onopen = () => { handlers.onOpen?.(); }; ws.onmessage = (event) => { try { handlers.onMessage(JSON.parse(event.data as string) as OracleWSMessage); } catch { // Ignore malformed messages from the transport. } }; ws.onclose = () => { handlers.onClose(); if (!stopped) { retryTimeout = setTimeout(() => { handlers.onReconnect?.(); connect(); }, 3000); } }; ws.onerror = () => { ws.close(); }; } connect(); return () => { stopped = true; if (retryTimeout) clearTimeout(retryTimeout); ws?.close(); }; }