icebear commited on
Commit
19e1649
·
unverified ·
2 Parent(s): 5d0a52fab21b87

Merge pull request #1 from icebear0828/refactor/code-audit-cleanup

Browse files
config/default.yaml CHANGED
@@ -29,6 +29,10 @@ environment:
29
  default_id: null
30
  default_branch: "main"
31
 
 
 
 
 
32
  streaming:
33
  status_as_content: false
34
  chunk_size: 100
 
29
  default_id: null
30
  default_branch: "main"
31
 
32
+ session:
33
+ ttl_minutes: 60
34
+ cleanup_interval_minutes: 5
35
+
36
  streaming:
37
  status_as_content: false
38
  chunk_size: 100
src/auth/chatgpt-oauth.ts CHANGED
@@ -247,12 +247,13 @@ export async function loginViaCli(): Promise<{
247
 
248
  // Step 1: Send the initialize handshake
249
  const config = getConfig();
 
250
  sendRpc(
251
  "initialize",
252
  {
253
  clientInfo: {
254
- name: "Codex Desktop",
255
- title: "Codex Desktop",
256
  version: config.client.app_version,
257
  },
258
  },
@@ -380,12 +381,13 @@ export async function refreshTokenViaCli(): Promise<string> {
380
 
381
  // Send initialize
382
  const config = getConfig();
 
383
  sendRpc(
384
  "initialize",
385
  {
386
  clientInfo: {
387
- name: "Codex Desktop",
388
- title: "Codex Desktop",
389
  version: config.client.app_version,
390
  },
391
  },
 
247
 
248
  // Step 1: Send the initialize handshake
249
  const config = getConfig();
250
+ const originator = config.client.originator;
251
  sendRpc(
252
  "initialize",
253
  {
254
  clientInfo: {
255
+ name: originator,
256
+ title: originator,
257
  version: config.client.app_version,
258
  },
259
  },
 
381
 
382
  // Send initialize
383
  const config = getConfig();
384
+ const originator = config.client.originator;
385
  sendRpc(
386
  "initialize",
387
  {
388
  clientInfo: {
389
+ name: originator,
390
+ title: originator,
391
  version: config.client.app_version,
392
  },
393
  },
src/auth/manager.ts DELETED
@@ -1,165 +0,0 @@
1
- import { readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync } from "fs";
2
- import { resolve, dirname } from "path";
3
- import { randomBytes } from "crypto";
4
- import { getConfig } from "../config.js";
5
- import {
6
- decodeJwtPayload,
7
- extractChatGptAccountId,
8
- extractUserProfile,
9
- isTokenExpired,
10
- } from "./jwt-utils.js";
11
-
12
- interface PersistedAuth {
13
- token: string;
14
- proxyApiKey: string | null;
15
- userInfo: { email?: string; accountId?: string; planType?: string } | null;
16
- }
17
-
18
- const AUTH_FILE = resolve(process.cwd(), "data", "auth.json");
19
-
20
- export class AuthManager {
21
- private token: string | null = null;
22
- private userInfo: { email?: string; accountId?: string; planType?: string } | null = null;
23
- private proxyApiKey: string | null = null;
24
- private refreshLock: Promise<string | null> | null = null;
25
-
26
- constructor() {
27
- this.loadPersisted();
28
-
29
- // Override with config jwt_token if set
30
- const config = getConfig();
31
- if (config.auth.jwt_token) {
32
- this.setToken(config.auth.jwt_token);
33
- }
34
-
35
- // Override with env var if set
36
- const envToken = process.env.CODEX_JWT_TOKEN;
37
- if (envToken) {
38
- this.setToken(envToken);
39
- }
40
- }
41
-
42
- async getToken(forceRefresh?: boolean): Promise<string | null> {
43
- if (forceRefresh || (this.token && this.isExpired())) {
44
- // Use a lock to prevent concurrent refresh attempts
45
- if (!this.refreshLock) {
46
- this.refreshLock = this.attemptRefresh();
47
- }
48
- try {
49
- return await this.refreshLock;
50
- } finally {
51
- this.refreshLock = null;
52
- }
53
- }
54
- return this.token;
55
- }
56
-
57
- setToken(token: string): void {
58
- this.token = token;
59
-
60
- // Extract user info from JWT claims
61
- const profile = extractUserProfile(token);
62
- const accountId = extractChatGptAccountId(token);
63
- this.userInfo = {
64
- email: profile?.email,
65
- accountId: accountId ?? undefined,
66
- planType: profile?.chatgpt_plan_type,
67
- };
68
-
69
- // Generate proxy API key if we don't have one yet
70
- if (!this.proxyApiKey) {
71
- this.proxyApiKey = this.generateApiKey();
72
- }
73
-
74
- this.persist();
75
- }
76
-
77
- clearToken(): void {
78
- this.token = null;
79
- this.userInfo = null;
80
- this.proxyApiKey = null;
81
- try {
82
- if (existsSync(AUTH_FILE)) {
83
- unlinkSync(AUTH_FILE);
84
- }
85
- } catch {
86
- // ignore cleanup errors
87
- }
88
- }
89
-
90
- isAuthenticated(): boolean {
91
- return this.token !== null && !this.isExpired();
92
- }
93
-
94
- getUserInfo(): { email?: string; accountId?: string; planType?: string } | null {
95
- return this.userInfo;
96
- }
97
-
98
- getAccountId(): string | null {
99
- if (!this.token) return null;
100
- return extractChatGptAccountId(this.token);
101
- }
102
-
103
- getProxyApiKey(): string | null {
104
- return this.proxyApiKey;
105
- }
106
-
107
- validateProxyApiKey(key: string): boolean {
108
- if (!this.proxyApiKey) return false;
109
- return key === this.proxyApiKey;
110
- }
111
-
112
- // --- private helpers ---
113
-
114
- private isExpired(): boolean {
115
- if (!this.token) return true;
116
- const config = getConfig();
117
- return isTokenExpired(this.token, config.auth.refresh_margin_seconds);
118
- }
119
-
120
- private async attemptRefresh(): Promise<string | null> {
121
- // We cannot auto-refresh without Codex CLI interaction.
122
- // If the token is expired, the user needs to re-login.
123
- if (this.token && isTokenExpired(this.token)) {
124
- this.token = null;
125
- this.userInfo = null;
126
- }
127
- return this.token;
128
- }
129
-
130
- private persist(): void {
131
- try {
132
- const dir = dirname(AUTH_FILE);
133
- if (!existsSync(dir)) {
134
- mkdirSync(dir, { recursive: true });
135
- }
136
- const data: PersistedAuth = {
137
- token: this.token!,
138
- proxyApiKey: this.proxyApiKey,
139
- userInfo: this.userInfo,
140
- };
141
- writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2), "utf-8");
142
- } catch {
143
- // Persist is best-effort
144
- }
145
- }
146
-
147
- private loadPersisted(): void {
148
- try {
149
- if (!existsSync(AUTH_FILE)) return;
150
- const raw = readFileSync(AUTH_FILE, "utf-8");
151
- const data = JSON.parse(raw) as PersistedAuth;
152
- if (data.token && typeof data.token === "string") {
153
- this.token = data.token;
154
- this.proxyApiKey = data.proxyApiKey ?? null;
155
- this.userInfo = data.userInfo ?? null;
156
- }
157
- } catch {
158
- // If the file is corrupt, start fresh
159
- }
160
- }
161
-
162
- private generateApiKey(): string {
163
- return "codex-proxy-" + randomBytes(24).toString("hex");
164
- }
165
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/config.ts CHANGED
@@ -35,6 +35,10 @@ const ConfigSchema = z.object({
35
  default_id: z.string().nullable().default(null),
36
  default_branch: z.string().default("main"),
37
  }),
 
 
 
 
38
  streaming: z.object({
39
  status_as_content: z.boolean().default(false),
40
  chunk_size: z.number().default(100),
 
35
  default_id: z.string().nullable().default(null),
36
  default_branch: z.string().default("main"),
37
  }),
38
+ session: z.object({
39
+ ttl_minutes: z.number().default(60),
40
+ cleanup_interval_minutes: z.number().default(5),
41
+ }),
42
  streaming: z.object({
43
  status_as_content: z.boolean().default(false),
44
  chunk_size: z.number().default(100),
src/proxy/client.ts DELETED
@@ -1,246 +0,0 @@
1
- /**
2
- * ProxyClient — fetch wrapper with auth headers, retry on 401, and SSE streaming.
3
- *
4
- * Mirrors the Codex Desktop ElectronFetchWrapper pattern.
5
- */
6
-
7
- import { getConfig } from "../config.js";
8
- import {
9
- buildHeaders,
10
- buildHeadersWithContentType,
11
- } from "../fingerprint/manager.js";
12
-
13
- export interface FetchOptions {
14
- method?: string;
15
- headers?: Record<string, string>;
16
- body?: string;
17
- signal?: AbortSignal;
18
- }
19
-
20
- export interface FetchResponse {
21
- status: number;
22
- headers: Record<string, string>;
23
- body: unknown;
24
- ok: boolean;
25
- }
26
-
27
- export class ProxyClient {
28
- private token: string;
29
- private accountId: string | null;
30
-
31
- constructor(token: string, accountId: string | null) {
32
- this.token = token;
33
- this.accountId = accountId;
34
- }
35
-
36
- /** Update the bearer token (e.g. after a refresh). */
37
- setToken(token: string): void {
38
- this.token = token;
39
- }
40
-
41
- /** Update the account ID. */
42
- setAccountId(accountId: string | null): void {
43
- this.accountId = accountId;
44
- }
45
-
46
- // ---- public helpers ----
47
-
48
- /** GET request, returns parsed JSON body. */
49
- async get(path: string): Promise<FetchResponse> {
50
- const url = this.ensureAbsoluteUrl(path);
51
- const res = await this.fetchWithRetry(url, {
52
- method: "GET",
53
- headers: buildHeaders(this.token, this.accountId),
54
- });
55
- const body = await res.json();
56
- return {
57
- status: res.status,
58
- headers: Object.fromEntries(res.headers.entries()),
59
- body,
60
- ok: res.ok,
61
- };
62
- }
63
-
64
- /** POST request with JSON body, returns parsed JSON body. */
65
- async post(path: string, body: unknown): Promise<FetchResponse> {
66
- const url = this.ensureAbsoluteUrl(path);
67
- const res = await this.fetchWithRetry(url, {
68
- method: "POST",
69
- headers: buildHeadersWithContentType(this.token, this.accountId),
70
- body: JSON.stringify(body),
71
- });
72
- const resBody = await res.json();
73
- return {
74
- status: res.status,
75
- headers: Object.fromEntries(res.headers.entries()),
76
- body: resBody,
77
- ok: res.ok,
78
- };
79
- }
80
-
81
- /** GET an SSE endpoint — yields parsed `{ event?, data }` objects. */
82
- async *stream(
83
- path: string,
84
- signal?: AbortSignal,
85
- ): AsyncGenerator<{ event?: string; data: unknown }> {
86
- const url = this.ensureAbsoluteUrl(path);
87
- const res = await this.fetchWithRetry(url, {
88
- method: "GET",
89
- headers: {
90
- ...buildHeaders(this.token, this.accountId),
91
- Accept: "text/event-stream",
92
- },
93
- signal,
94
- });
95
-
96
- if (!res.ok) {
97
- const text = await res.text();
98
- throw new Error(`SSE request failed (${res.status}): ${text}`);
99
- }
100
-
101
- if (!res.body) {
102
- throw new Error("Response body is null — cannot stream");
103
- }
104
-
105
- const reader = res.body
106
- .pipeThrough(new TextDecoderStream())
107
- .getReader();
108
-
109
- let buffer = "";
110
- try {
111
- while (true) {
112
- const { done, value } = await reader.read();
113
- if (done) break;
114
-
115
- buffer += value;
116
-
117
- // Process complete SSE messages (separated by double newline)
118
- const parts = buffer.split("\n\n");
119
- // Last part may be incomplete — keep it in the buffer
120
- buffer = parts.pop()!;
121
-
122
- for (const part of parts) {
123
- if (!part.trim()) continue;
124
- for (const parsed of this.parseSSE(part)) {
125
- if (parsed.data === "[DONE]") return;
126
- try {
127
- yield { event: parsed.event, data: JSON.parse(parsed.data) };
128
- } catch {
129
- yield { event: parsed.event, data: parsed.data };
130
- }
131
- }
132
- }
133
- }
134
-
135
- // Process any remaining data in the buffer
136
- if (buffer.trim()) {
137
- for (const parsed of this.parseSSE(buffer)) {
138
- if (parsed.data === "[DONE]") return;
139
- try {
140
- yield { event: parsed.event, data: JSON.parse(parsed.data) };
141
- } catch {
142
- yield { event: parsed.event, data: parsed.data };
143
- }
144
- }
145
- }
146
- } finally {
147
- reader.releaseLock();
148
- }
149
- }
150
-
151
- // ---- internal helpers ----
152
-
153
- /**
154
- * Resolve a relative URL to absolute using the configured base_url.
155
- * Mirrors Codex's ensureAbsoluteUrl.
156
- */
157
- private ensureAbsoluteUrl(url: string): string {
158
- if (/^https?:\/\//i.test(url) || url.startsWith("data:")) return url;
159
- const base = getConfig().api.base_url;
160
- return `${base}/${url.replace(/^\/+/, "")}`;
161
- }
162
-
163
- /**
164
- * Fetch with a single 401 retry (re-builds auth headers on retry).
165
- */
166
- private async fetchWithRetry(
167
- url: string,
168
- options: FetchOptions,
169
- onRefreshToken?: () => Promise<string | null>,
170
- ): Promise<Response> {
171
- const config = getConfig();
172
- const timeout = config.api.timeout_seconds * 1000;
173
-
174
- const doFetch = (opts: FetchOptions): Promise<Response> => {
175
- const controller = new AbortController();
176
- const timer = setTimeout(() => controller.abort(), timeout);
177
- const mergedSignal = opts.signal
178
- ? AbortSignal.any([opts.signal, controller.signal])
179
- : controller.signal;
180
-
181
- return fetch(url, {
182
- method: opts.method ?? "GET",
183
- headers: opts.headers,
184
- body: opts.body,
185
- signal: mergedSignal,
186
- }).finally(() => clearTimeout(timer));
187
- };
188
-
189
- const res = await doFetch(options);
190
-
191
- // Single retry on 401 if a refresh callback is provided
192
- if (res.status === 401 && onRefreshToken) {
193
- const newToken = await onRefreshToken();
194
- if (newToken) {
195
- this.token = newToken;
196
- const retryHeaders = options.headers?.["Content-Type"]
197
- ? buildHeadersWithContentType(this.token, this.accountId)
198
- : buildHeaders(this.token, this.accountId);
199
- return doFetch({ ...options, headers: retryHeaders });
200
- }
201
- }
202
-
203
- return res;
204
- }
205
-
206
- /**
207
- * Parse raw SSE text block into individual events.
208
- */
209
- private *parseSSE(
210
- text: string,
211
- ): Generator<{ event?: string; data: string }> {
212
- let event: string | undefined;
213
- let dataLines: string[] = [];
214
-
215
- for (const line of text.split("\n")) {
216
- if (line.startsWith("event:")) {
217
- event = line.slice(6).trim();
218
- } else if (line.startsWith("data:")) {
219
- dataLines.push(line.slice(5).trimStart());
220
- } else if (line === "" && dataLines.length > 0) {
221
- yield { event, data: dataLines.join("\n") };
222
- event = undefined;
223
- dataLines = [];
224
- }
225
- }
226
-
227
- // Yield any remaining accumulated data
228
- if (dataLines.length > 0) {
229
- yield { event, data: dataLines.join("\n") };
230
- }
231
- }
232
- }
233
-
234
- /**
235
- * Replace `{param}` placeholders in a URL template with encoded values.
236
- */
237
- export function serializePath(
238
- template: string,
239
- params: Record<string, string>,
240
- ): string {
241
- let path = template;
242
- for (const [key, value] of Object.entries(params)) {
243
- path = path.replace(`{${key}}`, encodeURIComponent(value));
244
- }
245
- return path;
246
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/routes/models.ts CHANGED
@@ -143,11 +143,14 @@ export function getModelCatalog(): CodexModelInfo[] {
143
 
144
  // --- Routes ---
145
 
 
 
 
146
  function toOpenAIModel(info: CodexModelInfo): OpenAIModel {
147
  return {
148
  id: info.id,
149
  object: "model",
150
- created: 1700000000,
151
  owned_by: "openai",
152
  };
153
  }
@@ -155,11 +158,11 @@ function toOpenAIModel(info: CodexModelInfo): OpenAIModel {
155
  app.get("/v1/models", (c) => {
156
  // Include catalog models + aliases as separate entries
157
  const models: OpenAIModel[] = MODEL_CATALOG.map(toOpenAIModel);
158
- for (const [alias, target] of Object.entries(MODEL_ALIASES)) {
159
  models.push({
160
  id: alias,
161
  object: "model",
162
- created: 1700000000,
163
  owned_by: "openai",
164
  });
165
  }
@@ -180,7 +183,7 @@ app.get("/v1/models/:modelId", (c) => {
180
  return c.json({
181
  id: modelId,
182
  object: "model",
183
- created: 1700000000,
184
  owned_by: "openai",
185
  });
186
  }
 
143
 
144
  // --- Routes ---
145
 
146
+ /** Stable timestamp used for all model `created` fields (2023-11-14T22:13:20Z). */
147
+ const MODEL_CREATED_TIMESTAMP = 1700000000;
148
+
149
  function toOpenAIModel(info: CodexModelInfo): OpenAIModel {
150
  return {
151
  id: info.id,
152
  object: "model",
153
+ created: MODEL_CREATED_TIMESTAMP,
154
  owned_by: "openai",
155
  };
156
  }
 
158
  app.get("/v1/models", (c) => {
159
  // Include catalog models + aliases as separate entries
160
  const models: OpenAIModel[] = MODEL_CATALOG.map(toOpenAIModel);
161
+ for (const [alias] of Object.entries(MODEL_ALIASES)) {
162
  models.push({
163
  id: alias,
164
  object: "model",
165
+ created: MODEL_CREATED_TIMESTAMP,
166
  owned_by: "openai",
167
  });
168
  }
 
183
  return c.json({
184
  id: modelId,
185
  object: "model",
186
+ created: MODEL_CREATED_TIMESTAMP,
187
  owned_by: "openai",
188
  });
189
  }
src/session/manager.ts CHANGED
@@ -1,4 +1,5 @@
1
  import { createHash } from "crypto";
 
2
 
3
  interface Session {
4
  taskId: string;
@@ -11,10 +12,10 @@ export class SessionManager {
11
  private sessions = new Map<string, Session>();
12
  private ttlMs: number;
13
 
14
- constructor(ttlMinutes: number = 60) {
15
- this.ttlMs = ttlMinutes * 60 * 1000;
16
- // Periodically clean expired sessions
17
- setInterval(() => this.cleanup(), 5 * 60 * 1000);
18
  }
19
 
20
  /**
 
1
  import { createHash } from "crypto";
2
+ import { getConfig } from "../config.js";
3
 
4
  interface Session {
5
  taskId: string;
 
12
  private sessions = new Map<string, Session>();
13
  private ttlMs: number;
14
 
15
+ constructor() {
16
+ const { ttl_minutes, cleanup_interval_minutes } = getConfig().session;
17
+ this.ttlMs = ttl_minutes * 60 * 1000;
18
+ setInterval(() => this.cleanup(), cleanup_interval_minutes * 60 * 1000);
19
  }
20
 
21
  /**