/** * HMAC-signed POSTs to the plugin's /wp-json/wp-ide/v1/tool-exec * endpoint. The plugin issued the secret when it called POST /v1/runs; * we use it to sign every tool callback so the plugin can verify the * caller is us. */ import { createHmac } from 'node:crypto'; import { logger } from '../lib/logger.js'; export interface ToolExecRequest { call_id: string; name: string; arguments: Record; } export interface ToolExecResponse { ok: boolean; call_id: string; result?: unknown; error?: string; } export async function runToolOnSite( callbackUrl: string, runId: string, secret: string, payload: ToolExecRequest, timeoutMs = 60_000, ): Promise { const body = JSON.stringify(payload); const signature = createHmac('sha256', secret).update(body).digest('hex'); const url = callbackUrl; const controller = new AbortController(); const t = setTimeout(() => controller.abort(), timeoutMs); const started = Date.now(); try { const res = await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json', 'accept': 'application/json', 'accept-encoding': 'identity', 'user-agent': 'wpide-server/0.2', 'x-wpide-run-id': runId, 'x-wpide-signature': signature, }, body, signal: controller.signal, }); const ms = Date.now() - started; const text = await res.text(); let parsed: ToolExecResponse; try { parsed = JSON.parse(text) as ToolExecResponse; } catch { const respHeaders: Record = {}; for (const [k, v] of res.headers.entries()) respHeaders[k] = v; logger.error({ url, status: res.status, ms, bodyLength: text.length, body: text.slice(0, 500), headers: respHeaders, }, 'tool-exec: non-JSON response'); return { ok: false, call_id: payload.call_id, error: `tool-exec non-JSON (HTTP ${res.status})` }; } if (!res.ok) { logger.warn({ status: res.status, ms, error: parsed.error }, 'tool-exec returned error status'); } return parsed; } catch (err) { return { ok: false, call_id: payload.call_id, error: `tool-exec fetch failed: ${(err as Error).message}` }; } finally { clearTimeout(t); } }