/** * COMPAT LAYER — temporary. Delete as the platform APIs improve. * * Action lifecycle management, working around today's API shapes: * - PATCH /service/{serviceId}/action/{actionId} { status, results } for * in_progress / success / failed (no dedicated action-status endpoint yet). * - The API expects a Bearer *token*, so NP_API_KEY is exchanged for * an access token (POST /token) and cached — now via the shared ../auth module. * * Everything here is a shim for a rough edge. When the platform exposes a * first-class action-lifecycle API, this whole file goes away and the base * layer talks to it directly. Keep base-layer code OUT of here. */ import { accessToken } from "../auth"; import { cfg } from "../config"; const API_URL = () => cfg.apiUrl(); const API_KEY = () => cfg.apiKey(); export type ActionStatus = "in_progress" | "success" | "failed"; export interface ActionMessage { level: "info" | "warning" | "error"; message: string; } // patchAction PATCHes the action with any of status / results / messages. It's // the single entry point for reporting; status/messages helpers wrap it. async function patchAction( serviceId: string, actionId: string, body: Record ): Promise { if (!API_KEY()) return; // skip lifecycle if no API key (local dev / testing) if (!serviceId || !actionId) return; const url = `${API_URL()}/service/${serviceId}/action/${actionId}`; const send = async (token: string) => fetch(url, { method: "PATCH", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify(body), }); let res = await send(await accessToken()); // A warm worker's token can expire between calls — on 401, force a fresh one // and retry once before giving up. if (res.status === 401) { res = await send(await accessToken(true)); } if (!res.ok) { const text = await res.text().catch(() => ""); throw new Error( `Action PATCH failed: ${res.status} ${res.statusText}${ text ? ` — ${text}` : "" }` ); } } export async function patchActionStatus( serviceId: string, actionId: string, status: ActionStatus, results?: Record ): Promise { const body: Record = { status }; if (results) body.results = results; return patchAction(serviceId, actionId, body); } // patchActionMessages appends log lines to the action (what shows in the UI as // the action runs) — the SDK equivalent of `np service-action exec --live-report`. export async function patchActionMessages( serviceId: string, actionId: string, messages: ActionMessage[] ): Promise { if (messages.length === 0) return; return patchAction(serviceId, actionId, { messages }); } // ── Scope capabilities (the worker-managed `state` attribute) ───────────── // Same auth/retry discipline as patchAction: bearer token, one forced refresh // on 401. GET returns the scope's capabilities object; PATCH replaces it. async function scopeRequest( method: "GET" | "PATCH", scopeId: string, body?: Record ): Promise { const url = `${API_URL()}/scope/${scopeId}`; const send = async (token: string) => fetch(url, { method, headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, ...(body ? { body: JSON.stringify(body) } : {}), }); let res = await send(await accessToken()); if (res.status === 401) res = await send(await accessToken(true)); return res; } export async function getScopeCapabilities( scopeId: string ): Promise> { if (!API_KEY() || !scopeId) return {}; const res = await scopeRequest("GET", scopeId); if (!res.ok) throw new Error(`Scope GET failed: ${res.status} ${res.statusText}`); const scope = (await res.json()) as { capabilities?: Record; }; return scope.capabilities ?? {}; } export async function patchScopeCapabilities( scopeId: string, capabilities: Record ): Promise { if (!API_KEY() || !scopeId) return; const res = await scopeRequest("PATCH", scopeId, { capabilities }); if (!res.ok) { const text = await res.text().catch(() => ""); throw new Error( `Scope PATCH failed: ${res.status} ${res.statusText}${ text ? ` — ${text}` : "" }` ); } }