From fd4ec9e5b8971da0b74cb04845d388175e24e218 Mon Sep 17 00:00:00 2001 From: decolua Date: Thu, 19 Mar 2026 15:32:29 +0700 Subject: [PATCH] - Enhance passthrough function to support response inspection - Add cursor tool configuration and update related components --- package.json | 6 +- .../dashboard/cli-tools/CLIToolsPageClient.js | 4 +- .../dashboard/mitm/MitmPageClient.js | 6 +- src/app/api/oauth/cursor/auto-import/route.js | 90 ++++++-------- src/mitm/config.js | 3 + src/mitm/dev | 2 +- src/mitm/dns/dnsConfig.js | 1 + src/mitm/manager.js | 5 + src/mitm/server.js | 29 ++++- src/shared/components/Sidebar.js | 2 +- src/shared/constants/cliTools.js | 114 ++++++++++-------- src/shared/services/initializeApp.js | 7 +- 12 files changed, 153 insertions(+), 116 deletions(-) diff --git a/package.json b/package.json index e815d19..b470bb8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "9router-app", - "version": "0.3.57", + "version": "0.3.58", "description": "9Router web dashboard", "private": true, "scripts": { @@ -15,7 +15,6 @@ "@monaco-editor/react": "^4.7.0", "@xyflow/react": "^12.10.1", "bcryptjs": "^3.0.3", - "better-sqlite3": "^12.6.2", "confbox": "^0.2.4", "express": "^5.2.1", "fs": "^0.0.1-security", @@ -34,16 +33,15 @@ "recharts": "^3.7.0", "selfsigned": "^5.5.0", "socks-proxy-agent": "^8.0.5", + "sql.js": "^1.14.1", "undici": "^7.19.2", "uuid": "^13.0.0", "zustand": "^5.0.10" }, "devDependencies": { "@tailwindcss/postcss": "^4.1.18", - "esbuild": "^0.27.4", "eslint": "^9", "eslint-config-next": "16.1.6", - "javascript-obfuscator": "^5.3.0", "postcss": "^8.5.6", "tailwindcss": "^4" } diff --git a/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.js b/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.js index dc07aa8..b5d03f5 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.js +++ b/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.js @@ -8,8 +8,6 @@ import { ClaudeToolCard, CodexToolCard, DroidToolCard, OpenClawToolCard, Default const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL; -// MITM tools are now on /dashboard/mitm — exclude from CLI Tools page -const MITM_TOOL_IDS = ["antigravity", "copilot"]; const STATUS_ENDPOINTS = { claude: "/api/cli-tools/claude-settings", @@ -185,7 +183,7 @@ export default function CLIToolsPageClient({ machineId }) { } }; - const regularTools = Object.entries(CLI_TOOLS).filter(([id]) => !MITM_TOOL_IDS.includes(id)); + const regularTools = Object.entries(CLI_TOOLS); return (
diff --git a/src/app/(dashboard)/dashboard/mitm/MitmPageClient.js b/src/app/(dashboard)/dashboard/mitm/MitmPageClient.js index 8634658..6849359 100644 --- a/src/app/(dashboard)/dashboard/mitm/MitmPageClient.js +++ b/src/app/(dashboard)/dashboard/mitm/MitmPageClient.js @@ -1,13 +1,11 @@ "use client"; import { useState, useEffect } from "react"; -import { CLI_TOOLS } from "@/shared/constants/cliTools"; +import { MITM_TOOLS } from "@/shared/constants/cliTools"; import { getModelsByProviderId } from "@/shared/constants/models"; import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers"; import { MitmServerCard, MitmToolCard } from "@/app/(dashboard)/dashboard/cli-tools/components"; -const MITM_TOOL_IDS = ["antigravity", "copilot", "kiro"]; - export default function MitmPageClient() { const [connections, setConnections] = useState([]); const [apiKeys, setApiKeys] = useState([]); @@ -74,7 +72,7 @@ export default function MitmPageClient() { ); }; - const mitmTools = Object.entries(CLI_TOOLS).filter(([id]) => MITM_TOOL_IDS.includes(id)); + const mitmTools = Object.entries(MITM_TOOLS); return (
diff --git a/src/app/api/oauth/cursor/auto-import/route.js b/src/app/api/oauth/cursor/auto-import/route.js index c638abf..c41b9ec 100644 --- a/src/app/api/oauth/cursor/auto-import/route.js +++ b/src/app/api/oauth/cursor/auto-import/route.js @@ -1,10 +1,9 @@ import { NextResponse } from "next/server"; -import { access, constants } from "fs/promises"; +import { access, constants, readFile } from "fs/promises"; import { homedir } from "os"; import { join } from "path"; -import { execFile, execSync } from "child_process"; +import { execFile } from "child_process"; import { promisify } from "util"; -import { createRequire } from "module"; const execFileAsync = promisify(execFile); @@ -39,23 +38,35 @@ function getCandidatePaths(platform) { ]; } -/** Extract tokens using better-sqlite3 (stream-based, no RAM limit) */ -function extractTokens(db) { - const desiredKeys = [...ACCESS_TOKEN_KEYS, ...MACHINE_ID_KEYS]; - const rows = db.prepare( - `SELECT key, value FROM itemTable WHERE key IN (${desiredKeys.map(() => "?").join(",")})` - ).all(...desiredKeys); +const normalize = (value) => { + if (typeof value !== "string") return value; + try { + const parsed = JSON.parse(value); + return typeof parsed === "string" ? parsed : value; + } catch { + return value; + } +}; - const normalize = (value) => { - if (typeof value !== "string") return value; - try { - const parsed = JSON.parse(value); - return typeof parsed === "string" ? parsed : value; - } catch { - return value; - } +/** Extract tokens using sql.js (pure JS, cross-platform) */ +async function extractTokens(dbPath) { + const initSqlJs = (await import("sql.js")).default; + const SQL = await initSqlJs(); + const db = new SQL.Database(await readFile(dbPath)); + + const queryAll = (sql, params = []) => { + const stmt = db.prepare(sql); + stmt.bind(params); + const rows = []; + while (stmt.step()) rows.push(stmt.getAsObject()); + stmt.free(); + return rows; }; + const desiredKeys = [...ACCESS_TOKEN_KEYS, ...MACHINE_ID_KEYS]; + const placeholders = desiredKeys.map(() => "?").join(","); + const rows = queryAll(`SELECT key, value FROM itemTable WHERE key IN (${placeholders})`, desiredKeys); + const tokens = {}; for (const row of rows) { if (ACCESS_TOKEN_KEYS.includes(row.key) && !tokens.accessToken) { @@ -67,22 +78,18 @@ function extractTokens(db) { // Fuzzy fallback if (!tokens.accessToken || !tokens.machineId) { - const fallbackRows = db.prepare( + const fallbackRows = queryAll( "SELECT key, value FROM itemTable WHERE key LIKE '%cursorAuth/%' OR key LIKE '%machineId%' OR key LIKE '%serviceMachineId%'" - ).all(); - + ); for (const row of fallbackRows) { const key = row.key || ""; const value = normalize(row.value); - if (!tokens.accessToken && key.toLowerCase().includes("accesstoken")) { - tokens.accessToken = value; - } - if (!tokens.machineId && key.toLowerCase().includes("machineid")) { - tokens.machineId = value; - } + if (!tokens.accessToken && key.toLowerCase().includes("accesstoken")) tokens.accessToken = value; + if (!tokens.machineId && key.toLowerCase().includes("machineid")) tokens.machineId = value; } } + db.close(); return tokens; } @@ -154,34 +161,13 @@ export async function GET() { }); } - // Strategy 1: better-sqlite3 bundled → then global install fallback - let Database = null; + // Strategy 1: sql.js (pure JS WASM, cross-platform) try { - const mod = await import("better-sqlite3"); - Database = mod.default; - } catch { - // Try loading from global node_modules (user ran: npm i better-sqlite3 -g) - try { - const globalRoot = execSync("npm root -g", { timeout: 5000, windowsHide: true }).toString().trim(); - const requireGlobal = createRequire(join(globalRoot, "better-sqlite3", "package.json")); - Database = requireGlobal("better-sqlite3"); - } catch { /* fall through to sqlite3 CLI strategy */ } - } - - if (Database) { - let db; - try { - db = new Database(dbPath, { readonly: true, fileMustExist: true }); - const tokens = extractTokens(db); - db.close(); - - if (tokens.accessToken && tokens.machineId) { - return NextResponse.json({ found: true, accessToken: tokens.accessToken, machineId: tokens.machineId }); - } - } catch { - db?.close(); + const tokens = await extractTokens(dbPath); + if (tokens.accessToken && tokens.machineId) { + return NextResponse.json({ found: true, accessToken: tokens.accessToken, machineId: tokens.machineId }); } - } + } catch { /* fall through to sqlite3 CLI strategy */ } // Strategy 2: sqlite3 CLI (works on Windows if sqlite3 is installed) try { diff --git a/src/mitm/config.js b/src/mitm/config.js index 896c621..29229d9 100644 --- a/src/mitm/config.js +++ b/src/mitm/config.js @@ -5,12 +5,14 @@ const TARGET_HOSTS = [ "cloudcode-pa.googleapis.com", "api.individual.githubcopilot.com", "q.us-east-1.amazonaws.com", + "api2.cursor.sh", ]; const URL_PATTERNS = { antigravity: [":generateContent", ":streamGenerateContent"], copilot: ["/chat/completions", "/v1/messages", "/responses"], kiro: ["/generateAssistantResponse"], + cursor: ["/BidiAppend", "/RunSSE", "/RunPoll", "/Run"], }; function getToolForHost(host) { @@ -18,6 +20,7 @@ function getToolForHost(host) { if (h === "api.individual.githubcopilot.com") return "copilot"; if (h === "daily-cloudcode-pa.googleapis.com" || h === "cloudcode-pa.googleapis.com") return "antigravity"; if (h === "q.us-east-1.amazonaws.com") return "kiro"; + if (h === "api2.cursor.sh") return "cursor"; return null; } diff --git a/src/mitm/dev b/src/mitm/dev index 39f5376..ebfb215 160000 --- a/src/mitm/dev +++ b/src/mitm/dev @@ -1 +1 @@ -Subproject commit 39f5376ac15cf062772a4f91cd7e81f2c30a2cf0 +Subproject commit ebfb215130fd06f9623ef75a5b41fc41a8ea2f2f diff --git a/src/mitm/dns/dnsConfig.js b/src/mitm/dns/dnsConfig.js index fc42a82..3050896 100644 --- a/src/mitm/dns/dnsConfig.js +++ b/src/mitm/dns/dnsConfig.js @@ -9,6 +9,7 @@ const TOOL_HOSTS = { antigravity: ["daily-cloudcode-pa.googleapis.com", "cloudcode-pa.googleapis.com"], copilot: ["api.individual.githubcopilot.com"], kiro: ["q.us-east-1.amazonaws.com", "codewhisperer.us-east-1.amazonaws.com"], + cursor: ["api2.cursor.sh"], }; const IS_WIN = process.platform === "win32"; diff --git a/src/mitm/manager.js b/src/mitm/manager.js index f24a7c3..571099f 100644 --- a/src/mitm/manager.js +++ b/src/mitm/manager.js @@ -464,6 +464,11 @@ async function startServer(apiKey, sudoPassword) { err(msg); startError = msg; } + // Detect wrong/missing password — clear cache and stop retry loop + if (!IS_WIN && (msg.includes("incorrect password") || msg.includes("no password was provided"))) { + setCachedPassword(null); + mitmIsRestarting = true; // prevent scheduleMitmRestart from firing + } }); serverProcess.on("exit", (code) => { log(`Server exited (code: ${code})`); diff --git a/src/mitm/server.js b/src/mitm/server.js index 3bde1c0..154e166 100644 --- a/src/mitm/server.js +++ b/src/mitm/server.js @@ -26,6 +26,7 @@ const handlers = { antigravity: loadHandler("antigravity"), copilot: loadHandler("copilot"), kiro: loadHandler("kiro"), + cursor: loadHandler("cursor"), }; // ── SSL / SNI ───────────────────────────────────────────────── @@ -118,7 +119,12 @@ function saveRequestLog(url, bodyBuffer) { } catch { /* ignore */ } } -async function passthrough(req, res, bodyBuffer) { +/** + * Forward request to real upstream. + * Optional onResponse(rawBuffer) callback — if provided, tees the response + * so it's both forwarded to client AND passed to the callback for inspection. + */ +async function passthrough(req, res, bodyBuffer, onResponse) { const targetHost = (req.headers.host || TARGET_HOSTS[0]).split(":")[0]; const targetIP = await resolveTargetIP(targetHost); @@ -132,7 +138,19 @@ async function passthrough(req, res, bodyBuffer) { rejectUnauthorized: false }, (forwardRes) => { res.writeHead(forwardRes.statusCode, forwardRes.headers); - forwardRes.pipe(res); + + if (!onResponse) { + forwardRes.pipe(res); + return; + } + + // Tee: forward to client AND buffer for callback + const chunks = []; + forwardRes.on("data", chunk => { chunks.push(chunk); res.write(chunk); }); + forwardRes.on("end", () => { + res.end(); + try { onResponse(Buffer.concat(chunks), forwardRes.headers); } catch { /* ignore */ } + }); }); forwardReq.on("error", (e) => { @@ -172,6 +190,13 @@ const server = https.createServer(sslOptions, async (req, res) => { log(`🔍 [${tool}] url=${req.url} | bodyLen=${bodyBuffer.length}`); + // Cursor uses binary proto — model extraction not possible at this layer. + // Delegate directly to handler which decodes proto internally. + if (tool === "cursor") { + log(`⚡ intercept | cursor | proto`); + return handlers[tool].intercept(req, res, bodyBuffer, null, passthrough); + } + const model = extractModel(req.url, bodyBuffer); log(`🔍 [${tool}] model="${model}"`); diff --git a/src/shared/components/Sidebar.js b/src/shared/components/Sidebar.js index df225b4..4c18108 100644 --- a/src/shared/components/Sidebar.js +++ b/src/shared/components/Sidebar.js @@ -72,7 +72,7 @@ export default function Sidebar({ onClose }) { return ( <> -