diff --git a/AGENTS.md b/AGENTS.md index 949f196..81e6921 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,6 +41,20 @@ direction. - **Later policy:** human emulation — snappy first action (~30–80 ms), bulk actions randomized, opt-in risky fast mode, free-account limits by default, ability toggles. +## Auth persistence + +- Durable credential: the token file, `VESKSLOP_TOKEN_FILE` (default `/token`, mode 600, + gitignored). It is the **puppet account's** token; zzib is only the DM test target. +- `DATA_DIR` is pinned: `$VENCORD_USER_DATA_DIR`, else `$VESKSLOP_DATA_DIR`, else + `/veskslop` (`src/main/constants.ts`). It does not depend on the app/product name, so + renames and version bumps do not move the Electron profile where Discord stores the session. +- On startup the preload asks main for the token (sync IPC) and seeds `localStorage.token` before + the client boots, but only when no session exists. `VESKSLOP_FORCE_TOKEN=1` overrides. +- Recovery: if Discord invalidates the session, run `pnpm poc:qr` for a fresh token and update the + token file. +- Discord deletes the stored token when it thinks devtools opened; keep Vesktop's devtools detector + patch (`src/renderer/patches/devtoolsFixes.ts`) so this cannot cause random logouts. + ## How to proceed **Current milestone — puppeting bridge (testable without Electron).** @@ -88,7 +102,6 @@ MCP clients (agents) ──Streamable HTTP──▶ veskSlop control daemon ## Open questions - Headless Electron strategy: Xvfb vs offscreen rendering, and what to strip to lighten the client. -- Token injection path for the official client (localStorage seeding vs client QR UI). - Voice/video: what the client's native stack can be driven to do without UI interaction. - Scope and auth model of the future remote proxy client and web debug UI. - How much of the Vesktop Electron tree gets deleted once the daemon is self-sufficient. diff --git a/poc/README.md b/poc/README.md index cbdcb75..6d5520e 100644 --- a/poc/README.md +++ b/poc/README.md @@ -27,10 +27,14 @@ TOKEN_FILE=/path/to/token DEBUG=1 pnpm poc:qr Connects to `wss://remote-auth-gateway.discord.gg/?v=2`, renders the approval QR directly in the terminal, waits for a phone scan, exchanges the login ticket and writes the token to -`poc/.token` (mode 600, gitignored; override with `TOKEN_FILE`). No existing token required. +`poc/.token` (mode 600, gitignored; override with `TOKEN_FILE`). This is the **puppet account's** +token. No existing token required. If Discord responds with an hCaptcha challenge, the script prints the challenge and prompts for a `HCAPTCHA_KEY` (or takes it from the environment). +The headless client consumes this file on startup via `VESKSLOP_TOKEN_FILE` (default +`/token`, see [../AGENTS.md](../AGENTS.md) → Auth persistence). + ### 1. Vesktop control bridge — `src/control-bridge.ts` The MCP daemon. Exposes Streamable HTTP and forwards tool calls over the JSON-lines protocol on diff --git a/src/main/constants.ts b/src/main/constants.ts index 458e11b..20c891c 100644 --- a/src/main/constants.ts +++ b/src/main/constants.ts @@ -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. Seeded into the client before it boots. */ +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); diff --git a/src/main/ipc.ts b/src/main/ipc.ts index aee11f3..fd52136 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -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 diff --git a/src/preload/index.ts b/src/preload/index.ts index 6ed74f9..c2db704 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -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 already 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. diff --git a/src/shared/IpcEvents.ts b/src/shared/IpcEvents.ts index 35c672f..f9440c4 100644 --- a/src/shared/IpcEvents.ts +++ b/src/shared/IpcEvents.ts @@ -25,6 +25,8 @@ export const enum IpcEvents { GET_SETTINGS = "VCD_GET_SETTINGS", SET_SETTINGS = "VCD_SET_SETTINGS", + VESKSLOP_GET_TOKEN = "VCD_VESKSLOP_GET_TOKEN", + IS_USING_CUSTOM_VENCORD_DIR = "VCD_IS_USING_CUSTOM_VENCORD_DIR", SHOW_CUSTOM_VENCORD_DIR = "VCD_SHOW_CUSTOM_VENCORD_DIR", SELECT_VENCORD_DIR = "VCD_SELECT_VENCORD_DIR",