/** * Shared HTTP helpers for tool executors. * Calls loopback tablet API routes with Bearer + x-voice-internal headers. */ export class ToolHttpError extends Error { constructor(public status: number, public body: string) { super(`tool_http_${status}`) } } const baseUrl = () => `http://localhost:${process.env.PORT || '3000'}` export function headers(): Record { const key = process.env.VOICE_API_KEY || '' return { Authorization: `Bearer ${key}`, 'x-voice-internal': key, 'Content-Type': 'application/json', } } export async function tabletGet(path: string, params?: Record): Promise { const url = new URL(`${baseUrl()}${path}`) if (params) { for (const [k, v] of Object.entries(params)) { if (v !== undefined && v !== '') url.searchParams.set(k, v) } } const r = await fetch(url, { headers: headers(), cache: 'no-store' }) if (!r.ok) throw new ToolHttpError(r.status, await r.text().catch(() => '')) return r.json() } export async function tabletJson( method: 'POST' | 'PUT' | 'DELETE', path: string, body?: any, params?: Record, ): Promise { const url = new URL(`${baseUrl()}${path}`) if (params) { for (const [k, v] of Object.entries(params)) { if (v !== undefined && v !== '') url.searchParams.set(k, v) } } const r = await fetch(url, { method, headers: headers(), body: body !== undefined ? JSON.stringify(body) : undefined, }) if (!r.ok) throw new ToolHttpError(r.status, await r.text().catch(() => '')) return r.json() }