File size: 9,156 Bytes
5d0a52f
 
 
 
 
3c0eaf7
92c5df7
5d0a52f
 
91ee702
5d0a52f
 
 
d0eb8b9
 
8b777a2
5d0a52f
 
4ebb914
 
8068f1c
85aec43
7445795
e7285df
3d01305
5f8456f
 
4f2665c
df56b50
91ee702
 
5d0a52f
4a940a5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5d0a52f
 
4a940a5
 
5d0a52f
5f8456f
 
 
e7285df
 
 
3d01305
 
 
5d0a52f
 
 
 
4ebb914
5d0a52f
 
 
 
 
92c5df7
5d0a52f
 
91ee702
5d0a52f
 
5dd5107
4ebb914
 
 
 
8068f1c
f878eff
df56b50
 
5d0a52f
91ee702
5d0a52f
 
 
d0eb8b9
 
8068f1c
4ebb914
8b777a2
5d0a52f
 
 
4a940a5
 
5d0a52f
 
b7d4394
5d0a52f
 
 
 
 
 
b7d4394
 
5d0a52f
 
 
 
 
 
 
a931669
5d0a52f
 
8b777a2
5d0a52f
 
 
91ee702
 
 
7445795
 
85aec43
7445795
 
 
85aec43
5f8456f
4ebb914
 
4f2665c
df56b50
4f2665c
4ebb914
 
5f8456f
b1107bc
5d0a52f
 
 
 
 
b94940f
 
 
 
4a940a5
 
996585e
4a940a5
0c2aed2
4a940a5
4f2665c
91ee702
4a940a5
4ebb914
4a940a5
 
 
 
 
 
 
996585e
 
1b6fb15
b94940f
4a940a5
 
 
 
 
 
996585e
 
73fd0ce
996585e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4a940a5
 
b1107bc
347f81b
5d0a52f
347f81b
 
b1107bc
 
347f81b
 
 
 
 
 
4a940a5
 
b1107bc
 
4a940a5
 
 
 
 
5d0a52f
 
 
 
 
 
4a940a5
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
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();
  });
}