80 lines
2.3 KiB
TypeScript
80 lines
2.3 KiB
TypeScript
/**
|
|
* 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<string, unknown>;
|
|
}
|
|
|
|
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<ToolExecResponse> {
|
|
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<string, string> = {};
|
|
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);
|
|
}
|
|
}
|