Wire validated core bridge, migrate daemon to it, verify live

This commit is contained in:
Fisto
2026-09-11 14:15:03 +02:00
parent e62d3007d8
commit 7d6650ce4e
27 changed files with 754 additions and 266 deletions
+6 -1
View File
@@ -18,10 +18,15 @@ export const PORTABLE =
!existsSync(join(vesktopDir, "Uninstall Vesktop.exe"));
export const DATA_DIR =
process.env.VENCORD_USER_DATA_DIR || (PORTABLE ? join(vesktopDir, "Data") : join(app.getPath("userData")));
process.env.VENCORD_USER_DATA_DIR ||
process.env.VESKSLOP_DATA_DIR ||
(PORTABLE ? join(vesktopDir, "Data") : join(app.getPath("appData"), "veskslop"));
mkdirSync(DATA_DIR, { recursive: true });
/** Durable auth credential. The preload seeds it into the client before boot. */
export const TOKEN_FILE = process.env.VESKSLOP_TOKEN_FILE || join(DATA_DIR, "token");
export const SESSION_DATA_DIR = join(DATA_DIR, "sessionData");
app.setPath("sessionData", SESSION_DATA_DIR);
+12
View File
@@ -26,6 +26,7 @@ import { join } from "path";
import { IpcEvents } from "../shared/IpcEvents";
import { setBadgeCount } from "./appBadge";
import { autoStart } from "./autoStart";
import { TOKEN_FILE } from "./constants";
import { enableHardwareAcceleration } from "./main";
import { mainWin } from "./mainWindow";
import { Settings, State } from "./settings";
@@ -60,6 +61,17 @@ handleSync(IpcEvents.GET_SETTINGS, () => Settings.plain);
handleSync(IpcEvents.GET_VERSION, () => app.getVersion());
handleSync(IpcEvents.GET_ENABLE_HARDWARE_ACCELERATION, () => enableHardwareAcceleration);
handleSync(IpcEvents.VESKSLOP_GET_TOKEN, () => {
let token: string | undefined;
try {
token = readFileSync(TOKEN_FILE, "utf8").trim() || undefined;
} catch {
// No token file: the client falls back to its normal login.
}
return { token, force: process.env.VESKSLOP_FORCE_TOKEN === "1" };
});
handleSync(
IpcEvents.SUPPORTS_WINDOWS_TRANSPARENCY,
() => process.platform === "win32" && Number(release().split(".").pop()) >= 22621
+5 -3
View File
@@ -16,6 +16,7 @@ import { DATA_DIR } from "./constants";
import { createFirstLaunchTour } from "./firstLaunch";
import { createWindows, mainWin } from "./mainWindow";
import { registerMediaPermissionsHandler } from "./mediaPermissions";
import { initPuppetBridge } from "./puppetBridge";
import { registerScreenShareHandler } from "./screenShare";
import { Settings, State } from "./settings";
import { setAsDefaultProtocolClient } from "./utils/setAsDefaultProtocolClient";
@@ -105,6 +106,7 @@ function init() {
registerScreenShareHandler();
registerMediaPermissionsHandler();
initPuppetBridge();
bootstrap();
@@ -127,10 +129,10 @@ if (!app.requestSingleInstanceLock({ IS_DEV })) {
}
async function bootstrap() {
if (!Object.hasOwn(State.store, "firstLaunch")) {
createFirstLaunchTour();
} else {
if (process.env.VESKSLOP_HEADLESS === "1" || Object.hasOwn(State.store, "firstLaunch")) {
createWindows();
} else {
createFirstLaunchTour();
}
}
+148
View File
@@ -0,0 +1,148 @@
/*
* Veskslop core bridge — the only external control surface.
*
* The bridge accepts newline-delimited JSON. It validates the request shape,
* forwards the action to the renderer, and fans events out to subscribers.
* The renderer validates the action content. See src/renderer/puppet/actions.ts.
*/
import { app, ipcMain } from "electron";
import { rmSync } from "fs";
import { rm } from "fs/promises";
import { createServer, Server, Socket } from "net";
import { join } from "path";
import { IpcCommands, IpcEvents } from "shared/IpcEvents";
import {
PUPPET_LIMITS,
PuppetAction,
PuppetErrorCode,
type PuppetEvent,
type PuppetRequest,
type PuppetResult
} from "shared/puppetProtocol";
import { sendRendererCommand } from "./ipcCommands";
const { env, platform } = process;
export const PUPPET_SOCKET_PATH =
platform === "win32"
? "\\\\?\\pipe\\vesktop-puppet.sock"
: join(env.XDG_RUNTIME_DIR || env.TMPDIR || env.TMP || env.TEMP || "/tmp", "vesktop-puppet.sock");
const KNOWN_ACTIONS = new Set<string>([
PuppetAction.Ping,
PuppetAction.EventsSubscribe,
PuppetAction.StateGet,
PuppetAction.MessageSend,
PuppetAction.MessageRead,
PuppetAction.DmSend
]);
let server: Server | undefined;
const subscribers = new Set<Socket>();
export async function initPuppetBridge() {
await rm(PUPPET_SOCKET_PATH, { force: true });
server = createServer(socket => {
socket.setEncoding("utf-8");
let buffer = "";
socket.on("data", (chunk: string) => {
buffer += chunk;
if (buffer.length > PUPPET_LIMITS.maxRequestBytes) {
respond(socket, {
ok: false,
error: { code: PuppetErrorCode.PayloadTooLarge, message: "request too large" }
});
buffer = "";
socket.destroy();
return;
}
let newline: number;
while ((newline = buffer.indexOf("\n")) !== -1) {
const line = buffer.slice(0, newline).trim();
buffer = buffer.slice(newline + 1);
if (line) void handleLine(socket, line);
}
});
socket.on("error", () => subscribers.delete(socket));
socket.on("close", () => subscribers.delete(socket));
});
server.listen(PUPPET_SOCKET_PATH);
server.on("error", error => console.error("[puppet] socket error:", error));
ipcMain.on(IpcEvents.PUPPET_EVENT, (_event, event: PuppetEvent) => {
const line = JSON.stringify({ type: "event", event }) + "\n";
for (const subscriber of subscribers) {
try {
subscriber.write(line);
} catch {
subscribers.delete(subscriber);
}
}
});
console.log(`[puppet] bridge listening on ${PUPPET_SOCKET_PATH}`);
}
function respond(socket: Socket, response: Record<string, unknown>) {
socket.write(JSON.stringify(response) + "\n");
}
async function handleLine(socket: Socket, line: string) {
let request: PuppetRequest;
try {
request = JSON.parse(line);
} catch {
respond(socket, { ok: false, error: { code: PuppetErrorCode.InvalidRequest, message: "invalid JSON" } });
return;
}
const { id, action, params } = request;
if (typeof action !== "string" || !KNOWN_ACTIONS.has(action)) {
respond(socket, {
id,
ok: false,
error: { code: PuppetErrorCode.UnknownAction, message: `unknown action: ${action}` }
});
return;
}
if (action === PuppetAction.Ping) {
respond(socket, { id, ok: true, result: "pong" });
return;
}
if (action === PuppetAction.EventsSubscribe) {
subscribers.add(socket);
respond(socket, { id, ok: true, result: { subscribed: true } });
return;
}
try {
const result = await sendRendererCommand<PuppetResult>(IpcCommands.PUPPET_COMMAND, { action, params });
if (result?.ok === false) respond(socket, { id, ok: false, error: result.error });
else respond(socket, { id, ok: true, result: result?.result });
} catch (error) {
console.error("[puppet] action failed:", action, error);
respond(socket, {
id,
ok: false,
error: { code: PuppetErrorCode.CoreUnavailable, message: "core is not ready" }
});
}
}
app.on("will-quit", () => {
server?.close();
rmSync(PUPPET_SOCKET_PATH, { force: true });
});