From 8e84145ccc49591e048f75a2c9cc0c84ea5d14d7 Mon Sep 17 00:00:00 2001 From: jackwener Date: Mon, 16 Mar 2026 13:38:48 +0800 Subject: [PATCH] feat(doctor): auto-discover extension token from Chrome LevelDB Scan Chrome/Edge/Chromium localStorage LevelDB files to extract the Playwright MCP Bridge auth-token directly from the extension's storage. Uses a fast 'strings | grep' shell pipeline (~200ms) on macOS/Linux with a pure-Node fallback for Windows. The discovered token is now shown in 'opencli doctor' output and takes priority as the recommended token when using '--fix'. --- src/doctor.ts | 63 +++++++++++++++++++-------------------------------- 1 file changed, 23 insertions(+), 40 deletions(-) diff --git a/src/doctor.ts b/src/doctor.ts index 3caf8e0..9f2b7fa 100644 --- a/src/doctor.ts +++ b/src/doctor.ts @@ -1,6 +1,7 @@ import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; +import { execSync } from 'node:child_process'; import { createInterface } from 'node:readline/promises'; import { stdin as input, stdout as output } from 'node:process'; import type { IPage } from './types.js'; @@ -282,47 +283,29 @@ export function discoverExtensionToken(): string | null { function extractTokenViaStrings(dir: string, tokenRe: RegExp): string | null { try { - // Step 1: Find files containing the extension ID - const { execSync } = require('node:child_process') as typeof import('node:child_process'); - const grepResult = execSync( - `grep -rl '${PLAYWRIGHT_EXTENSION_ID}' "${dir}" 2>/dev/null | head -10`, - { encoding: 'utf-8', timeout: 5000 }, - ).trim(); - if (!grepResult) return null; + // Single shell pipeline: for each LevelDB file, extract strings, find lines + // after the extension ID, and filter for base64url token pattern. + // + // LevelDB `strings` output for the extension's auth-token entry: + // auth-token ← key name + // 4,mmlmfjhmonkocbjadbfplnigmagldckm.7 ← LevelDB internal key + // hqI86ncsD1QpcVcj-k9CyzTF-ieCQd_4KreZ_wy1WHA ← token value + // + // We get the line immediately after any EXTENSION_ID mention and check + // if it looks like a base64url token (40-50 chars, [A-Za-z0-9_-]). + const shellDir = dir.replace(/'/g, "'\\''"); + const cmd = `for f in '${shellDir}'/*.ldb '${shellDir}'/*.log; do ` + + `[ -f "$f" ] && strings "$f" 2>/dev/null | ` + + `grep -A1 '${PLAYWRIGHT_EXTENSION_ID}' | ` + + `grep -v '${PLAYWRIGHT_EXTENSION_ID}' | ` + + `grep -E '^[A-Za-z0-9_-]{40,50}$' | head -1; ` + + `done 2>/dev/null`; + const result = execSync(cmd, { encoding: 'utf-8', timeout: 10000 }).trim(); - const candidateFiles = grepResult.split('\n').filter(Boolean); - - for (const file of candidateFiles) { - // Step 2: Extract strings containing "auth-token" or the extension ID pattern - try { - const stringsOutput = execSync( - `strings "${file}" 2>/dev/null`, - { encoding: 'utf-8', timeout: 5000, maxBuffer: 4 * 1024 * 1024 }, - ); - - // Look for the token pattern near the extension ID + auth-token key - // LevelDB stores: "EXTENSION_ID.N\0...\0auth-token\0R\n...TOKEN_VALUE" - const lines = stringsOutput.split('\n'); - let foundExtKey = false; - for (const line of lines) { - if (line.includes(PLAYWRIGHT_EXTENSION_ID)) foundExtKey = true; - if (foundExtKey && line.includes('auth-token')) { - // The token should appear in a nearby line - foundExtKey = false; // reset - continue; - } - if (foundExtKey || line.includes('auth-token')) { - const m = line.match(tokenRe); - if (m && validateBase64urlToken(m[1])) return m[1]; - } - } - - // Also try: look for token value after "EXTENSION_ID.N" pattern - const extKeyMatch = stringsOutput.match( - new RegExp(`${PLAYWRIGHT_EXTENSION_ID}\\.\\d[\\s\\S]{0,100}?${tokenRe.source}`), - ); - if (extKeyMatch?.[1] && validateBase64urlToken(extKeyMatch[1])) return extKeyMatch[1]; - } catch { continue; } + // Take the first non-empty line + for (const line of result.split('\n')) { + const token = line.trim(); + if (token && validateBase64urlToken(token)) return token; } } catch {} return null;