import http from "http"; import { URL } from "url"; import { CODEX_CONFIG, TRAE_CONFIG, WINDSURF_CONFIG, ZED_HOSTED_CONFIG } from "../constants/oauth.js"; // Loopback origin guard for local callback proxies. // Legit OAuth redirects are top-level navigations (no `Origin` header); a cross-site // page issuing `fetch(..., {mode:"no-cors"})` to scan + hit 127.0.0.1 always sends // `Origin: https://attacker`. Reject any non-loopback Origin to block login-CSRF. function isLoopbackOrigin(origin) { if (!origin) return true; // navigation redirect — allow return /^http:\/\/(127\.0\.0\.1|localhost)(:\d+)?$/.test(origin); } /** * Start a local HTTP server to receive OAuth callback * @param {Function} onCallback - Called with query params when callback received * @param {number} fixedPort - Optional fixed port number (default: random) * @returns {Promise<{server: http.Server, port: number, close: Function}>} */ export function startLocalServer(onCallback, fixedPort = null) { return new Promise((resolve, reject) => { const server = http.createServer((req, res) => { const url = new URL(req.url, `http://localhost`); if (url.pathname === "/callback" || url.pathname === "/auth/callback") { const params = Object.fromEntries(url.searchParams); // Send success response to browser with auto-close attempt res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.end(` Authentication Successful

Authentication Successful

Closing in 3 seconds...

`); // Call callback with params onCallback(params); } else { res.writeHead(404); res.end("Not found"); } }); // Listen on fixed port or find available port const portToUse = fixedPort || 0; server.listen(portToUse, "127.0.0.1", () => { const { port } = server.address(); resolve({ server, port, close: () => server.close(), }); }); server.on("error", (err) => { if (err.code === "EADDRINUSE" && fixedPort) { reject(new Error(`Port ${fixedPort} is already in use. Please close other applications using this port.`)); } else { reject(err); } }); }); } /** * Wait for callback with timeout * @param {number} timeoutMs - Timeout in milliseconds * @returns {Promise} - Callback params */ export function waitForCallback(timeoutMs = 300000) { return new Promise((resolve, reject) => { let resolved = false; const timeout = setTimeout(() => { if (!resolved) { resolved = true; reject(new Error("Authentication timeout")); } }, timeoutMs); const onCallback = (params) => { if (!resolved) { resolved = true; clearTimeout(timeout); resolve(params); } }; // Return the callback function resolve.__onCallback = onCallback; }); } // Singleton proxy server for Codex OAuth callback on fixed port let codexProxyServer = null; let codexProxyTimeout = null; const CODEX_PROXY_TIMEOUT_MS = 300000; // 5 minutes const CODEX_PORT = CODEX_CONFIG.fixedPort; // Pending exchange sessions keyed by state — used by server-side exchange mode const pendingExchanges = new Map(); /** * Register a pending exchange session for server-side mode. * Modal client calls this before opening popup. */ export function registerCodexSession({ state, codeVerifier, redirectUri }) { if (!state || !codeVerifier || !redirectUri) return false; pendingExchanges.set(state, { codeVerifier, redirectUri, status: "pending", createdAt: Date.now(), }); return true; } /** * Read session status (modal polls this). */ export function getCodexSessionStatus(state) { return pendingExchanges.get(state) || null; } /** * Clear a session (called after modal consumes status). */ export function clearCodexSession(state) { pendingExchanges.delete(state); } function escapeHtml(str) { return String(str) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function renderCodexResultPage(success, message) { const color = success ? "#22c55e" : "#ef4444"; const icon = success ? "✓" : "✗"; const title = success ? "Authentication Successful" : "Authentication Failed"; const safeMessage = escapeHtml(message); return ` ${title}
${icon}

${title}

${safeMessage}

Closing in 3s...

`; } /** * Start Codex proxy on fixed port 1455. * Mode A (server-side): if any session was registered, proxy auto-exchanges + saves DB. * Mode B (channel fallback): if no session, proxy 302 redirects to app port for legacy channel-based flow. */ export function startCodexProxy(appPort) { return new Promise((resolve) => { if (codexProxyServer) { resolve({ success: true }); return; } const server = http.createServer(async (req, res) => { const url = new URL(req.url, "http://localhost"); if (url.pathname !== "/callback" && url.pathname !== "/auth/callback") { res.writeHead(404); res.end("Not found"); return; } const code = url.searchParams.get("code"); const state = url.searchParams.get("state"); const errorParam = url.searchParams.get("error"); const session = state ? pendingExchanges.get(state) : null; // Mode A: server-side exchange (session registered) if (session) { try { if (errorParam) { throw new Error(url.searchParams.get("error_description") || errorParam); } if (!code) throw new Error("No authorization code received"); // Lazy import to avoid circular deps const { exchangeTokens } = await import("../providers.js"); const { createProviderConnection } = await import("@/models"); const tokenData = await exchangeTokens( "codex", code, session.redirectUri, session.codeVerifier, state ); const connection = await createProviderConnection({ provider: "codex", authType: "oauth", ...tokenData, expiresAt: tokenData.expiresIn ? new Date(Date.now() + tokenData.expiresIn * 1000).toISOString() : null, testStatus: "active", }); session.status = "done"; session.connectionId = connection.id; session.email = connection.email; res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.end(renderCodexResultPage(true, "You can close this window.")); } catch (err) { session.status = "error"; session.error = err.message; res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.end(renderCodexResultPage(false, err.message)); } finally { stopCodexProxy(); } return; } // Mode B: legacy channel fallback — 302 redirect to app /callback const redirectUrl = `http://localhost:${appPort}/callback${url.search}`; res.writeHead(302, { Location: redirectUrl }); res.end(); stopCodexProxy(); }); server.listen(CODEX_PORT, "127.0.0.1", () => { codexProxyServer = server; codexProxyTimeout = setTimeout(() => stopCodexProxy(), CODEX_PROXY_TIMEOUT_MS); resolve({ success: true }); }); server.on("error", (err) => { if (err.code === "EADDRINUSE") { resolve({ success: false, reason: "port_busy" }); } else { resolve({ success: false, reason: err.message }); } }); }); } /** * Stop the Codex proxy server and cleanup */ export function stopCodexProxy() { if (codexProxyTimeout) { clearTimeout(codexProxyTimeout); codexProxyTimeout = null; } if (codexProxyServer) { codexProxyServer.close(); codexProxyServer = null; } } // ─────────────────────────────────────────────────────────────────────────── // xAI fixed-port proxy on 127.0.0.1:56121 // Same shape as the Codex proxy. Kept as a parallel implementation rather than // generalizing the Codex one to keep the codex hot-path byte-equivalent. // ─────────────────────────────────────────────────────────────────────────── let xaiProxyServer = null; let xaiProxyTimeout = null; const XAI_PROXY_TIMEOUT_MS = 300000; // 5 minutes const XAI_PROXY_PORT = 56121; const xaiPendingExchanges = new Map(); export function registerXaiSession({ state, codeVerifier, redirectUri }) { if (!state || !codeVerifier || !redirectUri) return false; xaiPendingExchanges.set(state, { codeVerifier, redirectUri, status: "pending", createdAt: Date.now(), }); return true; } export function getXaiSessionStatus(state) { return xaiPendingExchanges.get(state) || null; } export function clearXaiSession(state) { xaiPendingExchanges.delete(state); } function renderXaiResultPage(success, message) { return renderCodexResultPage(success, message); } /** * Start xAI proxy on fixed port 56121. * Mode A (server-side): if any session was registered, proxy auto-exchanges + saves DB. * Mode B (channel fallback): if no session, proxy 302 redirects to app port. */ export function startXaiProxy(appPort) { return new Promise((resolve) => { if (xaiProxyServer) { resolve({ success: true }); return; } const server = http.createServer(async (req, res) => { const url = new URL(req.url, "http://localhost"); if (url.pathname !== "/callback" && url.pathname !== "/auth/callback") { res.writeHead(404); res.end("Not found"); return; } const code = url.searchParams.get("code"); const state = url.searchParams.get("state"); const errorParam = url.searchParams.get("error"); const session = state ? xaiPendingExchanges.get(state) : null; // Mode A: server-side exchange if (session) { try { if (errorParam) { throw new Error(url.searchParams.get("error_description") || errorParam); } if (!code) throw new Error("No authorization code received"); const { exchangeTokens } = await import("../providers.js"); const { createProviderConnection } = await import("@/models"); const tokenData = await exchangeTokens( "xai", code, session.redirectUri, session.codeVerifier, state ); const connection = await createProviderConnection({ provider: "xai", authType: "oauth", ...tokenData, expiresAt: tokenData.expiresIn ? new Date(Date.now() + tokenData.expiresIn * 1000).toISOString() : null, testStatus: "active", }); session.status = "done"; session.connectionId = connection.id; session.email = connection.email; res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.end(renderXaiResultPage(true, "You can close this window.")); } catch (err) { session.status = "error"; session.error = err.message; res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.end(renderXaiResultPage(false, err.message)); } finally { stopXaiProxy(); } return; } // Mode B: legacy fallback redirect const redirectUrl = `http://localhost:${appPort}/callback${url.search}`; res.writeHead(302, { Location: redirectUrl }); res.end(); stopXaiProxy(); }); server.listen(XAI_PROXY_PORT, "127.0.0.1", () => { xaiProxyServer = server; xaiProxyTimeout = setTimeout(() => stopXaiProxy(), XAI_PROXY_TIMEOUT_MS); resolve({ success: true }); }); server.on("error", (err) => { if (err.code === "EADDRINUSE") { resolve({ success: false, reason: "port_busy" }); } else { resolve({ success: false, reason: err.message }); } }); }); } export function stopXaiProxy() { if (xaiProxyTimeout) { clearTimeout(xaiProxyTimeout); xaiProxyTimeout = null; } if (xaiProxyServer) { xaiProxyServer.close(); xaiProxyServer = null; } } // ─────────────────────────────────────────────────────────────────────────── // Trae dynamic-port proxy. Singleton session (one connect at a time per provider). // Callback path = /callback with params refreshToken + loginHost. // ─────────────────────────────────────────────────────────────────────────── let traeProxyServer = null; let traeProxyTimeout = null; let traeProxyPort = null; let traeSession = null; export function registerTraeSession({ state }) { if (!state) return false; traeSession = { state, status: "pending", createdAt: Date.now() }; return true; } export function getTraeSessionStatus(state) { if (!traeSession) return null; if (state && traeSession.state !== state) return null; return traeSession; } export function clearTraeSession(state) { if (!state || (traeSession && traeSession.state === state)) traeSession = null; } export function startTraeProxy() { return new Promise((resolve) => { if (traeProxyServer) { resolve({ success: true, port: traeProxyPort, callbackUrl: `http://127.0.0.1:${traeProxyPort}${TRAE_CONFIG.callbackPath}` }); return; } const server = http.createServer(async (req, res) => { const url = new URL(req.url, "http://localhost"); if (url.pathname !== TRAE_CONFIG.callbackPath && url.pathname !== "/auth/callback") { res.writeHead(404); res.end("Not found"); return; } const session = traeSession; if (!session) { res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.end(renderCodexResultPage(false, "No active Trae login session")); return; } // Anti-CSRF: reject cross-origin fetches (legit redirects send no Origin), // and reject state mismatch when state is present. if (!isLoopbackOrigin(req.headers.origin)) { res.writeHead(403, { "Content-Type": "text/html; charset=utf-8" }); res.end(renderCodexResultPage(false, "Cross-origin callback rejected")); return; } const cbState = url.searchParams.get("state"); if (cbState && session.state && cbState !== session.state) { session.status = "error"; session.error = "Trae callback state mismatch"; res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.end(renderCodexResultPage(false, session.error)); stopTraeProxy(); return; } // Pass the raw callback query to exchangeTokens → parseTraeCallback const rawCallback = `${url.pathname}?${url.searchParams.toString()}`; try { const { exchangeTokens } = await import("../providers.js"); const { createProviderConnection } = await import("@/models"); const tokenData = await exchangeTokens("trae", rawCallback); const connection = await createProviderConnection({ provider: "trae", authType: "oauth", ...tokenData, expiresAt: tokenData.expiresIn ? new Date(Date.now() + tokenData.expiresIn * 1000).toISOString() : null, testStatus: "active", }); session.status = "done"; session.connectionId = connection.id; session.email = connection.email; res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.end(renderCodexResultPage(true, "You can close this window.")); } catch (err) { session.status = "error"; session.error = err.message; res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.end(renderCodexResultPage(false, err.message)); } finally { stopTraeProxy(); } }); server.listen(0, "127.0.0.1", () => { traeProxyServer = server; traeProxyPort = server.address().port; traeProxyTimeout = setTimeout(() => stopTraeProxy(), TRAE_CONFIG.oauthTimeoutMs); resolve({ success: true, port: traeProxyPort, callbackUrl: `http://127.0.0.1:${traeProxyPort}${TRAE_CONFIG.callbackPath}` }); }); server.on("error", (err) => resolve({ success: false, reason: err.message })); }); } export function stopTraeProxy() { if (traeProxyTimeout) { clearTimeout(traeProxyTimeout); traeProxyTimeout = null; } if (traeProxyServer) { traeProxyServer.close(); traeProxyServer = null; } traeProxyPort = null; } // ─────────────────────────────────────────────────────────────────────────── // Windsurf dynamic-port proxy. Singleton session. // Callback path = /windsurf-auth-callback with params access_token (firebase JWT) + state. // ─────────────────────────────────────────────────────────────────────────── let windsurfProxyServer = null; let windsurfProxyTimeout = null; let windsurfProxyPort = null; let windsurfSession = null; export function registerWindsurfSession({ state }) { if (!state) return false; windsurfSession = { state, status: "pending", createdAt: Date.now() }; return true; } export function getWindsurfSessionStatus(state) { if (!windsurfSession) return null; if (state && windsurfSession.state !== state) return null; return windsurfSession; } export function clearWindsurfSession(state) { if (!state || (windsurfSession && windsurfSession.state === state)) windsurfSession = null; } export function startWindsurfProxy() { return new Promise((resolve) => { if (windsurfProxyServer) { resolve({ success: true, port: windsurfProxyPort, callbackUrl: `http://127.0.0.1:${windsurfProxyPort}${WINDSURF_CONFIG.callbackPath}` }); return; } const server = http.createServer(async (req, res) => { const url = new URL(req.url, "http://localhost"); if (url.pathname !== WINDSURF_CONFIG.callbackPath) { res.writeHead(404); res.end("Not found"); return; } const session = windsurfSession; if (!session) { res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.end(renderCodexResultPage(false, "No active Windsurf login session")); return; } // Anti-CSRF: reject cross-origin fetches, and require state present + matching. if (!isLoopbackOrigin(req.headers.origin)) { res.writeHead(403, { "Content-Type": "text/html; charset=utf-8" }); res.end(renderCodexResultPage(false, "Cross-origin callback rejected")); return; } const cbState = url.searchParams.get("state"); if (!cbState || !session.state || cbState !== session.state) { session.status = "error"; session.error = "Windsurf callback state mismatch"; res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.end(renderCodexResultPage(false, session.error)); stopWindsurfProxy(); return; } const rawCallback = `${url.pathname}?${url.searchParams.toString()}`; try { const { exchangeTokens } = await import("../providers.js"); const { createProviderConnection } = await import("@/models"); const tokenData = await exchangeTokens("windsurf", rawCallback, null, null, session.state); const connection = await createProviderConnection({ provider: "windsurf", authType: "api_key", ...tokenData, testStatus: "active", }); session.status = "done"; session.connectionId = connection.id; session.email = connection.email; res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.end(renderCodexResultPage(true, "You can close this window.")); } catch (err) { session.status = "error"; session.error = err.message; res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.end(renderCodexResultPage(false, err.message)); } finally { stopWindsurfProxy(); } }); server.listen(0, "127.0.0.1", () => { windsurfProxyServer = server; windsurfProxyPort = server.address().port; windsurfProxyTimeout = setTimeout(() => stopWindsurfProxy(), WINDSURF_CONFIG.oauthTimeoutMs); resolve({ success: true, port: windsurfProxyPort, callbackUrl: `http://127.0.0.1:${windsurfProxyPort}${WINDSURF_CONFIG.callbackPath}` }); }); server.on("error", (err) => resolve({ success: false, reason: err.message })); }); } export function stopWindsurfProxy() { if (windsurfProxyTimeout) { clearTimeout(windsurfProxyTimeout); windsurfProxyTimeout = null; } if (windsurfProxyServer) { windsurfProxyServer.close(); windsurfProxyServer = null; } windsurfProxyPort = null; } // ─────────────────────────────────────────────────────────────────────────── // Zed RSA native-app proxy. Singleton session. // Callback: GET http://127.0.0.1:/?user_id=...&access_token= // The proxy decrypts the access token using the private key stored in session.codeVerifier. // ─────────────────────────────────────────────────────────────────────────── let zedProxyServer = null; let zedProxyTimeout = null; let zedProxyPort = null; let zedSession = null; export function registerZedSession({ state, codeVerifier, systemId }) { if (!state || !codeVerifier) return false; zedSession = { state, codeVerifier, systemId: systemId || null, status: "pending", createdAt: Date.now(), }; return true; } export function getZedSessionStatus(state) { if (!zedSession) return null; if (state && zedSession.state !== state) return null; return zedSession; } export function clearZedSession(state) { if (!state || (zedSession && zedSession.state === state)) zedSession = null; } export function startZedProxy(preferredPort = 0) { return new Promise((resolve) => { if (zedProxyServer) { // Reuse the live listener, but renew its idle timeout so a previous // flow's deadline can never kill the flow that just adopted the port. if (zedProxyTimeout) clearTimeout(zedProxyTimeout); zedProxyTimeout = setTimeout(() => { console.log("[Zed proxy] timeout, stopping"); stopZedProxy(); }, ZED_HOSTED_CONFIG.oauthTimeoutMs); resolve({ success: true, port: zedProxyPort, callbackUrl: `http://127.0.0.1:${zedProxyPort}/` }); return; } const server = http.createServer(async (req, res) => { const url = new URL(req.url, "http://localhost"); // Log path + redacted params (access_token is the RSA-encrypted credential). const redacted = Object.fromEntries(url.searchParams); for (const k of ["access_token", "user_id", "code_verifier", "state"]) { if (redacted[k]) redacted[k] = ""; } console.log("[Zed proxy]", req.method, url.pathname, JSON.stringify(redacted)); if (url.pathname !== "/" && url.pathname !== "/callback") { res.writeHead(404); res.end("Not found"); return; } const session = zedSession; if (!session) { res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.end(renderCodexResultPage(false, "No active Zed login session")); return; } // Anti-CSRF: Zed tokens are RSA-encrypted to our keypair so they can't be // forged cross-site, but still reject cross-origin fetches for defense-in-depth. if (!isLoopbackOrigin(req.headers.origin)) { res.writeHead(403, { "Content-Type": "text/html; charset=utf-8" }); res.end(renderCodexResultPage(false, "Cross-origin callback rejected")); return; } // A genuine Zed redirect always carries user_id + access_token. Anything // else (probe, prefetch, stray navigation, favicon-style miss) is NOT // the callback: answer without touching the session and WITHOUT // stopping the server, so the real redirect can still land afterwards. const qp = url.searchParams; const hasZedParams = qp.has("user_id") || qp.has("userId") || qp.has("access_token") || qp.has("accessToken") || qp.has("token"); if (!hasZedParams) { console.log(`[Zed proxy] ignoring non-callback ${req.method} ${url.pathname} (session kept, server kept)`); res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.end(renderCodexResultPage(false, "Waiting for Zed sign-in — this request carried no login data.")); return; } // Pass raw callback path+query to exchangeTokens → parseZedCallbackPayload. // codeVerifier carries the encoded RSA private key for decryption. const rawCallback = url.search ? `${url.pathname}?${url.searchParams.toString()}` : url.pathname; try { const { exchangeTokens } = await import("../providers.js"); const { createProviderConnection } = await import("@/models"); const tokenData = await exchangeTokens( "zed", rawCallback, null, session.codeVerifier, session.state, session.systemId ? { systemId: session.systemId } : undefined, ); const connection = await createProviderConnection({ provider: "zed", authType: "oauth", ...tokenData, testStatus: "active", }); session.status = "done"; session.connectionId = connection.id; session.email = connection.email; res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.end(renderCodexResultPage(true, "You can close this window.")); stopZedProxy(); } catch (err) { session.status = "error"; session.error = err.message; res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.end(renderCodexResultPage(false, err.message)); // Intentionally NOT stopping here: the failure may belong to a // superseded attempt (e.g. an older popup landing after "Try Again" // registered a new keypair). The live attempt's genuine callback must // still land. The idle timeout + modal close bound the listener. } }); const tryPort = Number(preferredPort) || 0; server.on("error", (err) => { // If the preferred port (e.g. 58443) is busy, fall back to a random port. if (err.code === "EADDRINUSE" && tryPort !== 0) { console.log(`[Zed proxy] port ${tryPort} busy, falling back to random`); server.listen(0, "127.0.0.1", () => { zedProxyServer = server; zedProxyPort = server.address().port; zedProxyTimeout = setTimeout(() => stopZedProxy(), ZED_HOSTED_CONFIG.oauthTimeoutMs); console.log(`[Zed proxy] listening on random port ${zedProxyPort}`); resolve({ success: true, port: zedProxyPort, callbackUrl: `http://127.0.0.1:${zedProxyPort}/` }); }); } else { console.log(`[Zed proxy] listen error: ${err.message}`); resolve({ success: false, reason: err.message }); } }); server.listen(tryPort, "127.0.0.1", () => { zedProxyServer = server; zedProxyPort = server.address().port; zedProxyTimeout = setTimeout(() => { console.log("[Zed proxy] timeout, stopping"); stopZedProxy(); }, ZED_HOSTED_CONFIG.oauthTimeoutMs); console.log(`[Zed proxy] listening on port ${zedProxyPort}`); resolve({ success: true, port: zedProxyPort, callbackUrl: `http://127.0.0.1:${zedProxyPort}/` }); }); }); } export function stopZedProxy() { console.log(`[Zed proxy] stopping (port ${zedProxyPort || "-"})`); if (zedProxyTimeout) { clearTimeout(zedProxyTimeout); zedProxyTimeout = null; } if (zedProxyServer) { zedProxyServer.close(); zedProxyServer = null; } zedProxyPort = null; } // ─────────────────────────────────────────────────────────────────────────── // Xiaomi MiMo Desktop OAuth callback proxy // Receives the ECDH-encrypted `u` param, decrypts it, stores the session. // ─────────────────────────────────────────────────────────────────────────── let xiaomiMimoProxyServer = null; let xiaomiMimoProxyPort = null; let xiaomiMimoProxyTimeout = null; const xiaomiMimoSessions = new Map(); export function registerXiaomiMimoSession({ state, privateKeyDer }) { if (!state || !privateKeyDer) return false; xiaomiMimoSessions.set(state, { privateKeyDer, status: "pending", createdAt: Date.now(), }); return true; } export function getXiaomiMimoSessionStatus(state) { const s = xiaomiMimoSessions.get(state); if (!s) return null; // Don't leak the private key to the client return { status: s.status, result: s.result || null, error: s.error || null }; } export function clearXiaomiMimoSession(state) { xiaomiMimoSessions.delete(state); } function renderXiaomiMimoResultPage(success, message) { const color = success ? "#22c55e" : "#ef4444"; const icon = success ? "✓" : "✗"; const title = success ? "Authentication Successful" : "Authentication Failed"; return ` ${title}
${icon}

${title}

${message || (success ? "You can close this tab and return to 9Router." : "Please try again.")}

${success ? "" : ""}
`; } /** * Start the Xiaomi Desktop OAuth callback proxy. * @returns {Promise<{success: boolean, port?: number, callbackUrl?: string, reason?: string}>} */ export function startXiaomiMimoProxy() { return new Promise((resolve) => { if (xiaomiMimoProxyServer) { resolve({ success: true, port: xiaomiMimoProxyPort, callbackUrl: `http://127.0.0.1:${xiaomiMimoProxyPort}/`, }); return; } const server = http.createServer(async (req, res) => { // Origin guard if (!isLoopbackOrigin(req.headers.origin)) { res.writeHead(403); res.end("Forbidden"); return; } const url = new URL(req.url, "http://127.0.0.1"); const u = url.searchParams.get("u"); if (!u) { res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" }); res.end(renderXiaomiMimoResultPage(false, "Missing encrypted payload (u parameter).")); return; } // Try each pending session's private key — the callback URL carries no // state param, so we attempt decryption with every pending key. const pendingSessions = [...xiaomiMimoSessions.entries()] .filter(([, s]) => s.status === "pending"); if (pendingSessions.length === 0) { res.writeHead(500, { "Content-Type": "text/html; charset=utf-8" }); res.end(renderXiaomiMimoResultPage(false, "No active OAuth session. Please restart the login flow.")); return; } try { const { decryptCallback } = await import("../providers/xiaomi-mimo.js"); let result = null; let matchedState = null; for (const [state, session] of pendingSessions) { try { result = decryptCallback(session.privateKeyDer, u); matchedState = state; break; } catch { // Wrong key for this session — try next } } if (!result || !matchedState) { throw new Error("Could not decrypt with any pending session key"); } if (!result.sk) { throw new Error("Decrypted payload missing sk (API key)"); } // Store result only in the matched session const session = xiaomiMimoSessions.get(matchedState); if (session) { session.status = "done"; session.result = { uid: result.uid, accessToken: result.sk, baseUrl: result.url || "https://api.xiaomimimo.com/v1", }; } res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.end(renderXiaomiMimoResultPage(true, "Xiaomi account linked. You can close this tab.")); console.log("[xiaomi-mimo oauth] callback decrypted, uid:", result.uid); } catch (err) { console.error("[xiaomi-mimo oauth] decrypt failed:", err.message); for (const [, session] of pendingSessions) { session.status = "error"; session.error = err.message; } res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" }); res.end(renderXiaomiMimoResultPage(false, `Decryption failed: ${err.message}`)); } }); server.on("error", (err) => { console.log("[xiaomi-mimo oauth] listen error:", err.message); resolve({ success: false, reason: err.message }); }); server.listen(0, "127.0.0.1", () => { xiaomiMimoProxyServer = server; xiaomiMimoProxyPort = server.address().port; xiaomiMimoProxyTimeout = setTimeout(() => { console.log("[xiaomi-mimo oauth] timeout, stopping"); stopXiaomiMimoProxy(); }, 300000); console.log(`[xiaomi-mimo oauth] listening on port ${xiaomiMimoProxyPort}`); resolve({ success: true, port: xiaomiMimoProxyPort, callbackUrl: `http://127.0.0.1:${xiaomiMimoProxyPort}/`, }); }); }); } export function stopXiaomiMimoProxy() { console.log(`[xiaomi-mimo oauth] stopping (port ${xiaomiMimoProxyPort || "-"})`); if (xiaomiMimoProxyTimeout) { clearTimeout(xiaomiMimoProxyTimeout); xiaomiMimoProxyTimeout = null; } if (xiaomiMimoProxyServer) { xiaomiMimoProxyServer.close(); xiaomiMimoProxyServer = null; } xiaomiMimoProxyPort = null; // No callback can arrive once the listener is down, so drop every pending // session — each holds an X25519 private key and they would otherwise // accumulate for the process lifetime (one per /authorize call). xiaomiMimoSessions.clear(); }