From 68d738659022d8f05cf9c9200343ce5aee5c1643 Mon Sep 17 00:00:00 2001 From: Duc Duong Date: Thu, 23 Apr 2026 08:16:28 +0700 Subject: [PATCH] Add --storage-state flag and storage-state-load command Loads a Playwright storageState JSON (cookies + per-origin localStorage) before executing any command. Enables auth flows where tokens live in localStorage rather than cookies (e.g. SPAs using localStorage JWTs). Usage: verdict --storage-state ./auth.json goto https://app.example.com verdict storage-state-load ./auth.json --- bin/browse.mjs | 18 +++++++++++++++++- src/server.mjs | 16 ++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/bin/browse.mjs b/bin/browse.mjs index a3392d2..4f6a8fa 100755 --- a/bin/browse.mjs +++ b/bin/browse.mjs @@ -17,14 +17,16 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const BASE_DATA_DIR = resolve(process.env.VERDICT_DATA_DIR || resolve(__dirname, '..', '.verdict-data')); const SERVER_SCRIPT = resolve(__dirname, '..', 'src', 'server.mjs'); -// Parse --session and --json from argv early +// Parse --session , --storage-state , and --json from argv early const rawArgs = process.argv.slice(2); const jsonFlag = rawArgs.includes('--json'); let sessionName = null; +let storageStatePath = null; const filtered = []; for (let i = 0; i < rawArgs.length; i++) { if (rawArgs[i] === '--json') continue; if (rawArgs[i] === '--session' && rawArgs[i + 1]) { sessionName = rawArgs[++i]; continue; } + if (rawArgs[i] === '--storage-state' && rawArgs[i + 1]) { storageStatePath = rawArgs[++i]; continue; } filtered.push(rawArgs[i]); } const [command, ...args] = filtered; @@ -117,6 +119,10 @@ Auth Profiles: handoff Switch to visible browser for manual login resume Return to headless after login goto-auth --profile Navigate with auto-loaded auth + storage-state-load Load Playwright storageState JSON (cookies + localStorage) + +Flags (global): + --storage-state Load Playwright storageState JSON before running command Cookies: cookies [url] List cookie-set Set manually @@ -191,11 +197,21 @@ if (!state || !isAlive(state.pid)) { } try { + if (storageStatePath) { + const loadResult = await send(state, 'storage-state-load', [storageStatePath]); + if (loadResult.startsWith('Error')) { outputError(loadResult); } + process.stderr.write(`${loadResult}\n`); + } output(await send(state, command, args)); } catch (e) { if (e.cause?.code === 'ECONNREFUSED' || e.cause?.code === 'UND_ERR_SOCKET' || e.message?.includes('fetch failed')) { process.stderr.write('Server down, restarting...\n'); state = await startServer(); + if (storageStatePath) { + const loadResult = await send(state, 'storage-state-load', [storageStatePath]); + if (loadResult.startsWith('Error')) { outputError(loadResult); } + process.stderr.write(`${loadResult}\n`); + } output(await send(state, command, args)); } else if (command === "stop") { output("Server stopped."); } else { outputError(e.message); } } diff --git a/src/server.mjs b/src/server.mjs index 249847c..db9e712 100644 --- a/src/server.mjs +++ b/src/server.mjs @@ -398,6 +398,22 @@ const commands = { const p = profilePath(name); if (!existsSync(p)) return `Profile "${name}" not found.`; unlinkSync(p); return `Profile "${name}" deleted.`; }, + async 'storage-state-load'(args) { + const filePath = args[0]; if (!filePath) return 'Error: path required. Usage: storage-state-load '; + if (!existsSync(filePath)) return `Error: file not found: ${filePath}`; + try { + const state = JSON.parse(readFileSync(filePath, 'utf8')); + await context.clearCookies(); + if (state.cookies?.length) await context.addCookies(state.cookies); + const origins = state.origins || []; + for (const { origin, localStorage: items } of origins) { + if (!items?.length) continue; + await page.goto(origin, { waitUntil: 'domcontentloaded', timeout: 15000 }).catch(() => {}); + await page.evaluate((ls) => { for (const { name, value } of ls) localStorage.setItem(name, value); }, items); + } + return `Loaded storageState from ${filePath}: ${state.cookies?.length || 0} cookies, ${origins.length} origin(s)`; + } catch (e) { return `Error loading storageState: ${e.message}`; } + }, async handoff() { const cookies = await context.cookies(); const currentUrl = page.url();