2026-01-04 21:58:59 -05:00
|
|
|
import http from "http";
|
|
|
|
|
import { URL } from "url";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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(`<!DOCTYPE html>
|
|
|
|
|
<html>
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="utf-8">
|
|
|
|
|
<title>Authentication Successful</title>
|
|
|
|
|
<style>
|
|
|
|
|
body { font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #f5f5f5; }
|
|
|
|
|
.container { text-align: center; padding: 2rem; background: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
|
|
|
|
.success { color: #22c55e; font-size: 3rem; }
|
|
|
|
|
h1 { margin: 1rem 0; }
|
|
|
|
|
p { color: #666; }
|
|
|
|
|
#countdown { font-weight: bold; }
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<div class="container">
|
|
|
|
|
<div class="success">✓</div>
|
|
|
|
|
<h1>Authentication Successful</h1>
|
|
|
|
|
<p id="message">Closing in <span id="countdown">3</span> seconds...</p>
|
|
|
|
|
</div>
|
|
|
|
|
<script>
|
|
|
|
|
let count = 3;
|
|
|
|
|
const countdown = document.getElementById("countdown");
|
|
|
|
|
const message = document.getElementById("message");
|
|
|
|
|
const timer = setInterval(() => {
|
|
|
|
|
count--;
|
|
|
|
|
countdown.textContent = count;
|
|
|
|
|
if (count <= 0) {
|
|
|
|
|
clearInterval(timer);
|
|
|
|
|
window.close();
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
message.textContent = "Please close this tab manually.";
|
|
|
|
|
}, 500);
|
|
|
|
|
}
|
|
|
|
|
}, 1000);
|
|
|
|
|
</script>
|
|
|
|
|
</body>
|
|
|
|
|
</html>`);
|
|
|
|
|
|
|
|
|
|
// 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<Object>} - 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;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 00:41:06 -04:00
|
|
|
// Singleton proxy server for Codex OAuth callback on fixed port
|
|
|
|
|
let codexProxyServer = null;
|
|
|
|
|
let codexProxyTimeout = null;
|
|
|
|
|
|
|
|
|
|
const CODEX_PROXY_TIMEOUT_MS = 300000; // 5 minutes
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Start a proxy server on Codex fixed port (1455) that redirects callback to the app port.
|
|
|
|
|
* Returns { success: true } if started, or { success: false } if port is busy.
|
|
|
|
|
*/
|
|
|
|
|
export function startCodexProxy(appPort) {
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
// Already running
|
|
|
|
|
if (codexProxyServer) {
|
|
|
|
|
resolve({ success: true });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const CODEX_PORT = 1455;
|
|
|
|
|
const server = http.createServer((req, res) => {
|
|
|
|
|
const url = new URL(req.url, "http://localhost");
|
|
|
|
|
|
|
|
|
|
if (url.pathname === "/callback" || url.pathname === "/auth/callback") {
|
|
|
|
|
// Redirect to app port with all query params preserved
|
|
|
|
|
const redirectUrl = `http://localhost:${appPort}/callback${url.search}`;
|
|
|
|
|
res.writeHead(302, { Location: redirectUrl });
|
|
|
|
|
res.end();
|
|
|
|
|
|
|
|
|
|
// Auto-close after redirect
|
|
|
|
|
stopCodexProxy();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
res.writeHead(404);
|
|
|
|
|
res.end("Not found");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
server.listen(CODEX_PORT, "127.0.0.1", () => {
|
|
|
|
|
codexProxyServer = server;
|
|
|
|
|
// Auto-cleanup after timeout
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|