Wire validated core bridge, migrate daemon to it, verify live
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
@@ -108,5 +108,8 @@ export const VesktopNative = {
|
||||
ipcRenderer.on(IpcEvents.IPC_COMMAND, (_, message) => cb(message));
|
||||
},
|
||||
respond: (response: IpcResponse) => ipcRenderer.send(IpcEvents.IPC_COMMAND, response)
|
||||
},
|
||||
puppet: {
|
||||
emit: (event: unknown) => ipcRenderer.send(IpcEvents.PUPPET_EVENT, event)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -9,6 +9,20 @@ import { contextBridge, ipcRenderer, webFrame } from "electron/renderer";
|
||||
import { IpcEvents } from "../shared/IpcEvents";
|
||||
import { VesktopNative } from "./VesktopNative";
|
||||
|
||||
// Veskslop: seed the Discord auth token before the client boots, unless a session exists.
|
||||
try {
|
||||
const seed = ipcRenderer.sendSync(IpcEvents.VESKSLOP_GET_TOKEN) as
|
||||
| { token?: string; force?: boolean }
|
||||
| undefined;
|
||||
|
||||
if (seed?.token && (seed.force || !localStorage.getItem("token"))) {
|
||||
localStorage.setItem("token", JSON.stringify(seed.token));
|
||||
console.log("[veskslop] seeded auth token");
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("[veskslop] failed to seed auth token", error);
|
||||
}
|
||||
|
||||
contextBridge.exposeInMainWorld("VesktopNative", VesktopNative);
|
||||
|
||||
// While sandboxed, Electron "polyfills" these APIs as local variables.
|
||||
|
||||
@@ -9,6 +9,7 @@ import "./ipcCommands";
|
||||
import "./appBadge";
|
||||
import "./fixes";
|
||||
import "./arrpc";
|
||||
import "./puppet";
|
||||
import "__patches__"; // auto generated by the build script
|
||||
|
||||
export * as Components from "./components";
|
||||
|
||||
@@ -8,6 +8,7 @@ import { SettingsRouter } from "@vencord/types/webpack/common";
|
||||
import { IpcCommands } from "shared/IpcEvents";
|
||||
|
||||
import { openScreenSharePicker } from "./components/ScreenSharePicker";
|
||||
import { handlePuppetAction } from "./puppet";
|
||||
|
||||
type IpcCommandHandler = (data: any) => any;
|
||||
|
||||
@@ -52,3 +53,5 @@ onIpcCommand(IpcCommands.NAVIGATE_SETTINGS, () => {
|
||||
onIpcCommand(IpcCommands.GET_LANGUAGES, () => navigator.languages);
|
||||
|
||||
onIpcCommand(IpcCommands.SCREEN_SHARE_PICKER, data => openScreenSharePicker(data.screens, data.skipPicker));
|
||||
|
||||
onIpcCommand(IpcCommands.PUPPET_COMMAND, data => handlePuppetAction(data.action, data.params));
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Veskslop core events — normalize client events and push them to main.
|
||||
*
|
||||
* The subscription starts only after Vencord registers the flux dispatcher.
|
||||
* A startup crash here would break the whole renderer bundle, so every step
|
||||
* is guarded.
|
||||
*/
|
||||
|
||||
import { ChannelStore } from "@vencord/types/webpack/common";
|
||||
import { filters, waitFor } from "@vencord/types/webpack";
|
||||
import type { PuppetEvent } from "shared/puppetProtocol";
|
||||
|
||||
function normalizeMessage(event: any): PuppetEvent | undefined {
|
||||
const message = event?.message;
|
||||
if (!message?.author) return undefined;
|
||||
|
||||
return {
|
||||
type: "MESSAGE_CREATE",
|
||||
id: message.id,
|
||||
channelId: event.channelId,
|
||||
guildId: event.guildId ?? null,
|
||||
channelType: ChannelStore.getChannel(event.channelId)?.type ?? null,
|
||||
author: {
|
||||
id: message.author.id,
|
||||
username: message.author.username,
|
||||
bot: !!message.author.bot
|
||||
},
|
||||
content: message.content ?? "",
|
||||
mentions: [
|
||||
...(message.mentions ?? []).map((user: any) => user.id),
|
||||
...(message.mentionIds ?? [])
|
||||
],
|
||||
timestamp: message.timestamp ?? new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
let installed = false;
|
||||
|
||||
export function initPuppetEvents() {
|
||||
waitFor(filters.byProps("dispatch", "subscribe"), dispatcher => {
|
||||
if (installed) return;
|
||||
installed = true;
|
||||
|
||||
dispatcher.subscribe("MESSAGE_CREATE", (event: any) => {
|
||||
try {
|
||||
const normalized = normalizeMessage(event);
|
||||
if (normalized) VesktopNative.puppet.emit(normalized);
|
||||
} catch (error) {
|
||||
console.error("[puppet] event normalization failed:", error);
|
||||
}
|
||||
});
|
||||
|
||||
console.log("[veskslop] puppet events active");
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* Veskslop core entry — starts the event push and exports the action handler.
|
||||
*/
|
||||
|
||||
import { initPuppetEvents } from "./events";
|
||||
|
||||
export { handlePuppetAction } from "./actions";
|
||||
|
||||
initPuppetEvents();
|
||||
@@ -62,7 +62,11 @@ export const enum IpcEvents {
|
||||
DEVTOOLS_OPENED = "VCD_DEVTOOLS_OPENED",
|
||||
DEVTOOLS_CLOSED = "VCD_DEVTOOLS_CLOSED",
|
||||
|
||||
CHOOSE_USER_ASSET = "VCD_CHOOSE_USER_ASSET"
|
||||
CHOOSE_USER_ASSET = "VCD_CHOOSE_USER_ASSET",
|
||||
|
||||
/* Veskslop */
|
||||
VESKSLOP_GET_TOKEN = "VCD_VESKSLOP_GET_TOKEN",
|
||||
PUPPET_EVENT = "VCD_PUPPET_EVENT"
|
||||
}
|
||||
|
||||
export const enum UpdaterIpcEvents {
|
||||
@@ -83,5 +87,8 @@ export const enum IpcCommands {
|
||||
|
||||
GET_LANGUAGES = "navigator.languages",
|
||||
|
||||
SCREEN_SHARE_PICKER = "screenshare:picker"
|
||||
SCREEN_SHARE_PICKER = "screenshare:picker",
|
||||
|
||||
/* Veskslop */
|
||||
PUPPET_COMMAND = "puppet:command"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user