codex-proxy / src /index.ts
icebear
feat: dashboard login gate for public deployments (#148)
91ee702 unverified
raw
history blame
9.16 kB
import { Hono } from "hono";
import { serve } from "@hono/node-server";
import { loadConfig, loadFingerprint, getConfig } from "./config.js";
import { AccountPool } from "./auth/account-pool.js";
import { RefreshScheduler } from "./auth/refresh-scheduler.js";
import { requestId } from "./middleware/request-id.js";
import { logger } from "./middleware/logger.js";
import { errorHandler } from "./middleware/error-handler.js";
import { dashboardAuth } from "./middleware/dashboard-auth.js";
import { createAuthRoutes } from "./routes/auth.js";
import { createAccountRoutes } from "./routes/accounts.js";
import { createChatRoutes } from "./routes/chat.js";
import { createMessagesRoutes } from "./routes/messages.js";
import { createGeminiRoutes } from "./routes/gemini.js";
import { createModelRoutes } from "./routes/models.js";
import { createWebRoutes } from "./routes/web.js";
import { CookieJar } from "./proxy/cookie-jar.js";
import { ProxyPool } from "./proxy/proxy-pool.js";
import { createProxyRoutes } from "./routes/proxies.js";
import { createResponsesRoutes } from "./routes/responses.js";
import { startUpdateChecker, stopUpdateChecker } from "./update-checker.js";
import { startProxyUpdateChecker, stopProxyUpdateChecker, setCloseHandler, getDeployMode } from "./self-update.js";
import { initProxy } from "./tls/curl-binary.js";
import { initTransport } from "./tls/transport.js";
import { loadStaticModels } from "./models/model-store.js";
import { startModelRefresh, stopModelRefresh } from "./models/model-fetcher.js";
import { startQuotaRefresh, stopQuotaRefresh } from "./auth/usage-refresher.js";
import { UsageStatsStore } from "./auth/usage-stats.js";
import { startSessionCleanup, stopSessionCleanup } from "./auth/dashboard-session.js";
import { createDashboardAuthRoutes } from "./routes/dashboard-login.js";
export interface ServerHandle {
close: () => Promise<void>;
port: number;
}
export interface StartOptions {
host?: string;
port?: number;
}
/**
* Core startup logic shared by CLI and Electron entry points.
* Throws on config errors instead of calling process.exit().
*/
export async function startServer(options?: StartOptions): Promise<ServerHandle> {
// Load configuration
console.log("[Init] Loading configuration...");
const config = loadConfig();
loadFingerprint();
// Load static model catalog (before transport/auth init)
loadStaticModels();
// Detect proxy (config > env > auto-detect local ports)
await initProxy();
// Initialize TLS transport (auto-selects curl CLI or libcurl FFI)
await initTransport();
// Initialize managers
const accountPool = new AccountPool();
const refreshScheduler = new RefreshScheduler(accountPool);
const cookieJar = new CookieJar();
const proxyPool = new ProxyPool();
// Create Hono app
const app = new Hono();
// Global middleware
app.use("*", requestId);
app.use("*", logger);
app.use("*", errorHandler);
app.use("*", dashboardAuth);
// Mount routes
const authRoutes = createAuthRoutes(accountPool, refreshScheduler);
const accountRoutes = createAccountRoutes(accountPool, refreshScheduler, cookieJar, proxyPool);
const chatRoutes = createChatRoutes(accountPool, cookieJar, proxyPool);
const messagesRoutes = createMessagesRoutes(accountPool, cookieJar, proxyPool);
const geminiRoutes = createGeminiRoutes(accountPool, cookieJar, proxyPool);
const responsesRoutes = createResponsesRoutes(accountPool, cookieJar, proxyPool);
const proxyRoutes = createProxyRoutes(proxyPool, accountPool);
const usageStats = new UsageStatsStore();
const webRoutes = createWebRoutes(accountPool, usageStats);
app.route("/", createDashboardAuthRoutes());
app.route("/", authRoutes);
app.route("/", accountRoutes);
app.route("/", chatRoutes);
app.route("/", messagesRoutes);
app.route("/", geminiRoutes);
app.route("/", responsesRoutes);
app.route("/", proxyRoutes);
app.route("/", createModelRoutes());
app.route("/", webRoutes);
// Start server
const port = options?.port ?? config.server.port;
const host = options?.host ?? config.server.host;
const poolSummary = accountPool.getPoolSummary();
const displayHost = (host === "0.0.0.0" || host === "::") ? "localhost" : host;
console.log(`
╔══════════════════════════════════════════╗
β•‘ Codex Proxy Server β•‘
╠══════════════════════════════════════════╣
β•‘ Status: ${accountPool.isAuthenticated() ? "Authenticated βœ“" : "Not logged in "} β•‘
β•‘ Listen: http://${displayHost}:${port} β•‘
β•‘ API: http://${displayHost}:${port}/v1 β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
`);
if (accountPool.isAuthenticated()) {
const user = accountPool.getUserInfo();
console.log(` User: ${user?.email ?? "unknown"}`);
console.log(` Plan: ${user?.planType ?? "unknown"}`);
console.log(` Key: ${config.server.proxy_api_key ?? accountPool.getProxyApiKey()}`);
console.log(` Pool: ${poolSummary.active} active / ${poolSummary.total} total accounts`);
} else {
console.log(` Open http://${displayHost}:${port} to login`);
}
console.log();
// Start dashboard session cleanup
startSessionCleanup();
// Start background update checkers
// (Electron has its own native auto-updater β€” skip proxy update checker)
startUpdateChecker();
if (getDeployMode() !== "electron") {
startProxyUpdateChecker();
}
// Start background model refresh (requires auth to be ready)
startModelRefresh(accountPool, cookieJar, proxyPool);
// Start background quota refresh
startQuotaRefresh(accountPool, cookieJar, proxyPool, usageStats);
// Start proxy health check timer (if proxies exist)
proxyPool.startHealthCheckTimer();
const server = serve({
fetch: app.fetch,
hostname: host,
port,
});
// Resolve actual port (may differ from requested when port=0)
const addr = server.address();
const actualPort = (addr && typeof addr === "object") ? addr.port : port;
const close = (): Promise<void> => {
return new Promise((resolve) => {
server.close(() => {
stopUpdateChecker();
stopProxyUpdateChecker();
stopModelRefresh();
stopQuotaRefresh();
stopSessionCleanup();
refreshScheduler.destroy();
proxyPool.destroy();
cookieJar.destroy();
accountPool.destroy();
resolve();
});
});
};
// Register close handler so self-update can attempt graceful shutdown before restart
setCloseHandler(close);
return { close, port: actualPort };
}
// ── CLI entry point ──────────────────────────────────────────────────
async function main() {
let handle: ServerHandle;
// Retry on EADDRINUSE β€” the previous process may still be releasing the port after a self-update restart
const MAX_RETRIES = 10;
const RETRY_DELAY_MS = 1000;
for (let attempt = 1; ; attempt++) {
try {
handle = await startServer();
break;
} catch (err) {
const code = (err as NodeJS.ErrnoException).code;
if (code === "EADDRINUSE" && attempt < MAX_RETRIES) {
console.warn(`[Init] Port in use, retrying in ${RETRY_DELAY_MS}ms (attempt ${attempt}/${MAX_RETRIES})...`);
await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
continue;
}
const msg = err instanceof Error ? err.message : String(err);
console.error(`[Init] Failed to start server: ${msg}`);
console.error("[Init] Make sure config/default.yaml and config/fingerprint.yaml exist and are valid YAML.");
process.exit(1);
}
}
// P1-7: Graceful shutdown β€” stop accepting, drain, then cleanup
let shutdownCalled = false;
const shutdown = () => {
if (shutdownCalled) return;
shutdownCalled = true;
console.log("\n[Shutdown] Stopping new connections...");
const forceExit = setTimeout(() => {
console.error("[Shutdown] Timeout after 10s β€” forcing exit");
process.exit(1);
}, 10_000);
if (forceExit.unref) forceExit.unref();
handle.close().then(() => {
console.log("[Shutdown] Server closed, cleanup complete.");
clearTimeout(forceExit);
process.exit(0);
}).catch((err) => {
console.error("[Shutdown] Error during cleanup:", err instanceof Error ? err.message : err);
clearTimeout(forceExit);
process.exit(1);
});
};
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
}
// Only run CLI entry when executed directly (not imported by Electron)
const isDirectRun = process.argv[1]?.includes("index");
if (isDirectRun) {
main().catch((err) => {
console.error("Fatal error:", err);
process.kill(process.pid, "SIGTERM");
setTimeout(() => process.exit(1), 2000).unref();
});
}