agentgui 1.0.752 → 1.0.754
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/oauth-codex.js +162 -0
- package/lib/oauth-common.js +92 -0
- package/lib/oauth-gemini.js +200 -0
- package/lib/speech-manager.js +203 -0
- package/package.json +1 -1
- package/server.js +30 -807
package/server.js
CHANGED
|
@@ -10,7 +10,6 @@ import { execSync, spawn } from 'child_process';
|
|
|
10
10
|
import { LRUCache } from 'lru-cache';
|
|
11
11
|
import { createRequire } from 'module';
|
|
12
12
|
const PKG_VERSION = JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version;
|
|
13
|
-
import { OAuth2Client } from 'google-auth-library';
|
|
14
13
|
import express from 'express';
|
|
15
14
|
import Busboy from 'busboy';
|
|
16
15
|
import fsbrowse from 'fsbrowse';
|
|
@@ -18,6 +17,9 @@ import { queries } from './database.js';
|
|
|
18
17
|
import { runClaudeWithStreaming } from './lib/claude-runner.js';
|
|
19
18
|
import { initializeDescriptors, getAgentDescriptor } from './lib/agent-descriptors.js';
|
|
20
19
|
import { findCommand, queryACPServerAgents, discoverAgents, discoverExternalACPServers, initializeAgentDiscovery } from './lib/agent-discovery.js';
|
|
20
|
+
import { getGeminiOAuthCreds, startGeminiOAuth, exchangeGeminiOAuthCode, handleGeminiOAuthCallback, getGeminiOAuthStatus, getGeminiOAuthState } from './lib/oauth-gemini.js';
|
|
21
|
+
import { initSpeechManager, getSpeech, ensurePocketTtsSetup, voiceCacheManager, modelDownloadState, broadcastModelProgress, ensureModelsDownloaded, eagerTTS } from './lib/speech-manager.js';
|
|
22
|
+
import { startCodexOAuth, exchangeCodexOAuthCode, handleCodexOAuthCallback, getCodexOAuthStatus, getCodexOAuthState, CODEX_HOME, CODEX_AUTH_FILE } from './lib/oauth-codex.js';
|
|
21
23
|
import { WSOptimizer } from './lib/ws-optimizer.js';
|
|
22
24
|
import { WsRouter } from './lib/ws-protocol.js';
|
|
23
25
|
import { encode as wsEncode } from './lib/codec.js';
|
|
@@ -68,232 +70,6 @@ process.on('SIGHUP', () => { console.log('[SIGNAL] SIGHUP received (ignored - un
|
|
|
68
70
|
process.on('beforeExit', (code) => { console.log('[PROCESS] beforeExit with code:', code); });
|
|
69
71
|
process.on('exit', (code) => { console.log('[PROCESS] exit with code:', code); });
|
|
70
72
|
|
|
71
|
-
const ttsTextAccumulators = new Map();
|
|
72
|
-
const voiceCacheManager = {
|
|
73
|
-
generating: new Map(),
|
|
74
|
-
maxCacheSize: 10 * 1024 * 1024,
|
|
75
|
-
async getOrGenerateCache(conversationId, text) {
|
|
76
|
-
const cacheKey = `${conversationId}:${text}`;
|
|
77
|
-
if (this.generating.has(cacheKey)) {
|
|
78
|
-
return new Promise((resolve) => {
|
|
79
|
-
const checkInterval = setInterval(() => {
|
|
80
|
-
const cached = queries.getVoiceCache(conversationId, text);
|
|
81
|
-
if (cached) {
|
|
82
|
-
clearInterval(checkInterval);
|
|
83
|
-
resolve(cached);
|
|
84
|
-
}
|
|
85
|
-
}, 50);
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
const cached = queries.getVoiceCache(conversationId, text);
|
|
89
|
-
if (cached) return cached;
|
|
90
|
-
this.generating.set(cacheKey, true);
|
|
91
|
-
try {
|
|
92
|
-
const speech = await getSpeech();
|
|
93
|
-
const audioBlob = await speech.synthesize(text, 'default');
|
|
94
|
-
const saved = queries.saveVoiceCache(conversationId, text, audioBlob);
|
|
95
|
-
const totalSize = queries.getVoiceCacheSize(conversationId);
|
|
96
|
-
if (totalSize > this.maxCacheSize) {
|
|
97
|
-
const needed = totalSize - this.maxCacheSize;
|
|
98
|
-
queries.deleteOldestVoiceCache(conversationId, needed);
|
|
99
|
-
}
|
|
100
|
-
return saved;
|
|
101
|
-
} finally {
|
|
102
|
-
this.generating.delete(cacheKey);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
let speechModule = null;
|
|
108
|
-
async function getSpeech() {
|
|
109
|
-
if (!speechModule) speechModule = await import('./lib/speech.js');
|
|
110
|
-
return speechModule;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async function ensurePocketTtsSetup(onProgress) {
|
|
114
|
-
const { createRequire: cr } = await import('module');
|
|
115
|
-
const r = cr(import.meta.url);
|
|
116
|
-
const serverTTS = r('webtalk/server-tts');
|
|
117
|
-
return serverTTS.ensureInstalled(onProgress);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Model download manager
|
|
121
|
-
const modelDownloadState = {
|
|
122
|
-
downloading: false,
|
|
123
|
-
progress: null,
|
|
124
|
-
error: null,
|
|
125
|
-
complete: false,
|
|
126
|
-
startTime: null,
|
|
127
|
-
downloadMetrics: new Map()
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
function broadcastModelProgress(progress) {
|
|
131
|
-
modelDownloadState.progress = progress;
|
|
132
|
-
const broadcastData = {
|
|
133
|
-
type: 'model_download_progress',
|
|
134
|
-
modelId: progress.type || 'unknown',
|
|
135
|
-
bytesDownloaded: progress.bytesDownloaded || 0,
|
|
136
|
-
bytesRemaining: progress.bytesRemaining || 0,
|
|
137
|
-
totalBytes: progress.totalBytes || 0,
|
|
138
|
-
downloadSpeed: progress.downloadSpeed || 0,
|
|
139
|
-
eta: progress.eta || 0,
|
|
140
|
-
retryCount: progress.retryCount || 0,
|
|
141
|
-
currentGateway: progress.currentGateway || '',
|
|
142
|
-
status: progress.status || (progress.done ? 'completed' : progress.downloading ? 'downloading' : 'paused'),
|
|
143
|
-
percentComplete: progress.percentComplete || 0,
|
|
144
|
-
completedFiles: progress.completedFiles || 0,
|
|
145
|
-
totalFiles: progress.totalFiles || 0,
|
|
146
|
-
timestamp: Date.now(),
|
|
147
|
-
...progress
|
|
148
|
-
};
|
|
149
|
-
broadcastSync(broadcastData);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async function validateAndCleanupModels(modelsDir) {
|
|
153
|
-
try {
|
|
154
|
-
const manifestPath = path.join(modelsDir, '.manifests.json');
|
|
155
|
-
if (fs.existsSync(manifestPath)) {
|
|
156
|
-
try {
|
|
157
|
-
const content = fs.readFileSync(manifestPath, 'utf8');
|
|
158
|
-
JSON.parse(content);
|
|
159
|
-
} catch (e) {
|
|
160
|
-
console.error('[MODELS] Manifest corrupted, removing:', e.message);
|
|
161
|
-
fs.unlinkSync(manifestPath);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const files = fs.readdirSync(modelsDir);
|
|
166
|
-
for (const file of files) {
|
|
167
|
-
if (file.endsWith('.tmp')) {
|
|
168
|
-
try {
|
|
169
|
-
fs.unlinkSync(path.join(modelsDir, file));
|
|
170
|
-
console.log('[MODELS] Cleaned up temp file:', file);
|
|
171
|
-
} catch (e) {
|
|
172
|
-
console.warn('[MODELS] Failed to clean:', file);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
} catch (e) {
|
|
177
|
-
console.warn('[MODELS] Cleanup check failed:', e.message);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
async function ensureModelsDownloaded() {
|
|
182
|
-
if (modelDownloadState.downloading) {
|
|
183
|
-
while (modelDownloadState.downloading) {
|
|
184
|
-
await new Promise(r => setTimeout(r, 100));
|
|
185
|
-
}
|
|
186
|
-
return modelDownloadState.complete;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
modelDownloadState.downloading = true;
|
|
190
|
-
modelDownloadState.error = null;
|
|
191
|
-
|
|
192
|
-
try {
|
|
193
|
-
const r = createRequire(import.meta.url);
|
|
194
|
-
const { createConfig } = r('webtalk/config');
|
|
195
|
-
const { ensureModel } = r('webtalk/whisper-models');
|
|
196
|
-
const { ensureTTSModels } = r('webtalk/tts-models');
|
|
197
|
-
const gmguiModels = path.join(os.homedir(), '.gmgui', 'models');
|
|
198
|
-
const modelsBase = process.env.PORTABLE_EXE_DIR
|
|
199
|
-
? (fs.existsSync(path.join(process.env.PORTABLE_EXE_DIR, 'models', 'onnx-community')) ? path.join(process.env.PORTABLE_EXE_DIR, 'models') : gmguiModels)
|
|
200
|
-
: gmguiModels;
|
|
201
|
-
|
|
202
|
-
await validateAndCleanupModels(modelsBase);
|
|
203
|
-
|
|
204
|
-
const config = createConfig({
|
|
205
|
-
modelsDir: modelsBase,
|
|
206
|
-
ttsModelsDir: path.join(modelsBase, 'tts'),
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
// Progress callback for broadcasting download progress
|
|
210
|
-
const onProgress = (progress) => {
|
|
211
|
-
broadcastModelProgress({
|
|
212
|
-
...progress,
|
|
213
|
-
started: true,
|
|
214
|
-
done: false,
|
|
215
|
-
downloading: true
|
|
216
|
-
});
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
broadcastModelProgress({ started: true, done: false, downloading: true, type: 'whisper', status: 'starting' });
|
|
220
|
-
await ensureModel('onnx-community/whisper-base', config, onProgress);
|
|
221
|
-
|
|
222
|
-
broadcastModelProgress({ started: true, done: false, downloading: true, type: 'tts', status: 'starting' });
|
|
223
|
-
await ensureTTSModels(config, onProgress);
|
|
224
|
-
|
|
225
|
-
modelDownloadState.complete = true;
|
|
226
|
-
broadcastModelProgress({ started: true, done: true, complete: true, downloading: false });
|
|
227
|
-
return true;
|
|
228
|
-
} catch (err) {
|
|
229
|
-
console.error('[MODELS] Download error:', err.message);
|
|
230
|
-
modelDownloadState.error = err.message;
|
|
231
|
-
broadcastModelProgress({ done: true, error: err.message });
|
|
232
|
-
return false;
|
|
233
|
-
} finally {
|
|
234
|
-
modelDownloadState.downloading = false;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function eagerTTS(text, conversationId, sessionId) {
|
|
239
|
-
const key = `${conversationId}:${sessionId}`;
|
|
240
|
-
let acc = ttsTextAccumulators.get(key);
|
|
241
|
-
if (!acc) {
|
|
242
|
-
acc = { text: '', timer: null };
|
|
243
|
-
ttsTextAccumulators.set(key, acc);
|
|
244
|
-
}
|
|
245
|
-
acc.text += text;
|
|
246
|
-
if (acc.timer) clearTimeout(acc.timer);
|
|
247
|
-
acc.timer = setTimeout(() => flushTTSaccumulator(key, conversationId, sessionId), 600);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function flushTTSaccumulator(key, conversationId, sessionId) {
|
|
251
|
-
const acc = ttsTextAccumulators.get(key);
|
|
252
|
-
if (!acc || !acc.text) return;
|
|
253
|
-
const text = acc.text.trim();
|
|
254
|
-
acc.text = '';
|
|
255
|
-
ttsTextAccumulators.delete(key);
|
|
256
|
-
|
|
257
|
-
getSpeech().then(speech => {
|
|
258
|
-
const status = speech.getStatus();
|
|
259
|
-
if (!status.ttsReady || status.ttsError) return;
|
|
260
|
-
const voices = new Set();
|
|
261
|
-
for (const ws of syncClients) {
|
|
262
|
-
const vid = ws.ttsVoiceId || 'default';
|
|
263
|
-
const convKey = `conv-${conversationId}`;
|
|
264
|
-
if (ws.subscriptions && (ws.subscriptions.has(sessionId) || ws.subscriptions.has(convKey))) {
|
|
265
|
-
voices.add(vid);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
if (voices.size === 0) return;
|
|
269
|
-
for (const vid of voices) {
|
|
270
|
-
const cacheKey = speech.ttsCacheKey(text, vid);
|
|
271
|
-
const cached = speech.ttsCacheGet(cacheKey);
|
|
272
|
-
if (cached) {
|
|
273
|
-
pushTTSAudio(cacheKey, cached, conversationId, sessionId, vid);
|
|
274
|
-
continue;
|
|
275
|
-
}
|
|
276
|
-
speech.synthesize(text, vid).then(wav => {
|
|
277
|
-
if (speech.ttsCacheSet) speech.ttsCacheSet(cacheKey, wav);
|
|
278
|
-
pushTTSAudio(cacheKey, wav, conversationId, sessionId, vid);
|
|
279
|
-
}).catch(() => {});
|
|
280
|
-
}
|
|
281
|
-
}).catch(() => {});
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function pushTTSAudio(cacheKey, wav, conversationId, sessionId, voiceId) {
|
|
285
|
-
const b64 = wav.toString('base64');
|
|
286
|
-
broadcastSync({
|
|
287
|
-
type: 'tts_audio',
|
|
288
|
-
cacheKey,
|
|
289
|
-
audio: b64,
|
|
290
|
-
voiceId,
|
|
291
|
-
conversationId,
|
|
292
|
-
sessionId,
|
|
293
|
-
timestamp: Date.now()
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
|
|
297
73
|
|
|
298
74
|
function buildSystemPrompt(agentId, model, subAgent) {
|
|
299
75
|
const parts = [];
|
|
@@ -483,551 +259,6 @@ async function getModelsForAgent(agentId) {
|
|
|
483
259
|
return models;
|
|
484
260
|
}
|
|
485
261
|
|
|
486
|
-
const GEMINI_SCOPES = [
|
|
487
|
-
'https://www.googleapis.com/auth/cloud-platform',
|
|
488
|
-
'https://www.googleapis.com/auth/userinfo.email',
|
|
489
|
-
'https://www.googleapis.com/auth/userinfo.profile',
|
|
490
|
-
];
|
|
491
|
-
|
|
492
|
-
function extractOAuthFromFile(oauth2Path) {
|
|
493
|
-
try {
|
|
494
|
-
const src = fs.readFileSync(oauth2Path, 'utf8');
|
|
495
|
-
const idMatch = src.match(/OAUTH_CLIENT_ID\s*=\s*['"]([^'"]+)['"]/);
|
|
496
|
-
const secretMatch = src.match(/OAUTH_CLIENT_SECRET\s*=\s*['"]([^'"]+)['"]/);
|
|
497
|
-
if (idMatch && secretMatch) return { clientId: idMatch[1], clientSecret: secretMatch[1] };
|
|
498
|
-
} catch {}
|
|
499
|
-
return null;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
function getGeminiOAuthCreds() {
|
|
503
|
-
if (process.env.GOOGLE_OAUTH_CLIENT_ID && process.env.GOOGLE_OAUTH_CLIENT_SECRET) {
|
|
504
|
-
return { clientId: process.env.GOOGLE_OAUTH_CLIENT_ID, clientSecret: process.env.GOOGLE_OAUTH_CLIENT_SECRET, custom: true };
|
|
505
|
-
}
|
|
506
|
-
const oauthRelPath = path.join('node_modules', '@google', 'gemini-cli-core', 'dist', 'src', 'code_assist', 'oauth2.js');
|
|
507
|
-
try {
|
|
508
|
-
const geminiPath = findCommand('gemini', rootDir);
|
|
509
|
-
if (geminiPath) {
|
|
510
|
-
const realPath = fs.realpathSync(geminiPath);
|
|
511
|
-
const pkgRoot = path.resolve(path.dirname(realPath), '..');
|
|
512
|
-
const result = extractOAuthFromFile(path.join(pkgRoot, oauthRelPath));
|
|
513
|
-
if (result) return result;
|
|
514
|
-
}
|
|
515
|
-
} catch (e) {
|
|
516
|
-
console.error('[gemini-oauth] gemini lookup failed:', e.message);
|
|
517
|
-
}
|
|
518
|
-
try {
|
|
519
|
-
const npmCacheDirs = new Set();
|
|
520
|
-
const addDir = (d) => { if (d) npmCacheDirs.add(path.join(d, '_npx')); };
|
|
521
|
-
addDir(path.join(os.homedir(), '.npm'));
|
|
522
|
-
addDir(path.join(os.homedir(), '.cache', '.npm'));
|
|
523
|
-
if (process.env.NPM_CACHE) addDir(process.env.NPM_CACHE);
|
|
524
|
-
if (process.env.npm_config_cache) addDir(process.env.npm_config_cache);
|
|
525
|
-
try { addDir(execSync('npm config get cache', { encoding: 'utf8', timeout: 5000 }).trim()); } catch {}
|
|
526
|
-
for (const cacheDir of npmCacheDirs) {
|
|
527
|
-
if (!fs.existsSync(cacheDir)) continue;
|
|
528
|
-
for (const d of fs.readdirSync(cacheDir).filter(d => !d.startsWith('.'))) {
|
|
529
|
-
const result = extractOAuthFromFile(path.join(cacheDir, d, oauthRelPath));
|
|
530
|
-
if (result) return result;
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
} catch (e) {
|
|
534
|
-
console.error('[gemini-oauth] npm cache scan failed:', e.message);
|
|
535
|
-
}
|
|
536
|
-
console.error('[gemini-oauth] Could not find Gemini CLI OAuth credentials in any known location');
|
|
537
|
-
return null;
|
|
538
|
-
}
|
|
539
|
-
const GEMINI_DIR = path.join(os.homedir(), '.gemini');
|
|
540
|
-
const GEMINI_OAUTH_FILE = path.join(GEMINI_DIR, 'oauth_creds.json');
|
|
541
|
-
const GEMINI_ACCOUNTS_FILE = path.join(GEMINI_DIR, 'google_accounts.json');
|
|
542
|
-
|
|
543
|
-
let geminiOAuthState = { status: 'idle', error: null, email: null };
|
|
544
|
-
let geminiOAuthPending = null;
|
|
545
|
-
|
|
546
|
-
function buildBaseUrl(req) {
|
|
547
|
-
const override = process.env.AGENTGUI_BASE_URL;
|
|
548
|
-
if (override) return override.replace(/\/+$/, '');
|
|
549
|
-
const fwdProto = req.headers['x-forwarded-proto'];
|
|
550
|
-
const fwdHost = req.headers['x-forwarded-host'] || req.headers['host'];
|
|
551
|
-
if (fwdHost) {
|
|
552
|
-
const proto = fwdProto || (req.socket.encrypted ? 'https' : 'http');
|
|
553
|
-
const cleanHost = fwdHost.replace(/:443$/, '').replace(/:80$/, '');
|
|
554
|
-
return `${proto}://${cleanHost}`;
|
|
555
|
-
}
|
|
556
|
-
return `http://127.0.0.1:${PORT}`;
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
function saveGeminiCredentials(tokens, email) {
|
|
560
|
-
if (!fs.existsSync(GEMINI_DIR)) fs.mkdirSync(GEMINI_DIR, { recursive: true });
|
|
561
|
-
fs.writeFileSync(GEMINI_OAUTH_FILE, JSON.stringify(tokens, null, 2), { mode: 0o600 });
|
|
562
|
-
try { fs.chmodSync(GEMINI_OAUTH_FILE, 0o600); } catch (_) {}
|
|
563
|
-
|
|
564
|
-
let accounts = { active: null, old: [] };
|
|
565
|
-
try {
|
|
566
|
-
if (fs.existsSync(GEMINI_ACCOUNTS_FILE)) {
|
|
567
|
-
accounts = JSON.parse(fs.readFileSync(GEMINI_ACCOUNTS_FILE, 'utf8'));
|
|
568
|
-
}
|
|
569
|
-
} catch (_) {}
|
|
570
|
-
|
|
571
|
-
if (email) {
|
|
572
|
-
if (accounts.active && accounts.active !== email && !accounts.old.includes(accounts.active)) {
|
|
573
|
-
accounts.old.push(accounts.active);
|
|
574
|
-
}
|
|
575
|
-
accounts.active = email;
|
|
576
|
-
}
|
|
577
|
-
fs.writeFileSync(GEMINI_ACCOUNTS_FILE, JSON.stringify(accounts, null, 2), { mode: 0o600 });
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
function geminiOAuthResultPage(title, message, success) {
|
|
581
|
-
const color = success ? '#10b981' : '#ef4444';
|
|
582
|
-
const icon = success ? '✓' : '✗';
|
|
583
|
-
return `<!DOCTYPE html><html><head><title>${title}</title></head>
|
|
584
|
-
<body style="margin:0;display:flex;align-items:center;justify-content:center;min-height:100vh;background:#111827;font-family:system-ui,sans-serif;color:white;">
|
|
585
|
-
<div style="text-align:center;max-width:400px;padding:2rem;">
|
|
586
|
-
<div style="font-size:4rem;color:${color};margin-bottom:1rem;">${icon}</div>
|
|
587
|
-
<h1 style="font-size:1.5rem;margin-bottom:0.5rem;">${title}</h1>
|
|
588
|
-
<p style="color:#9ca3af;">${message}</p>
|
|
589
|
-
<p style="color:#6b7280;margin-top:1rem;font-size:0.875rem;">You can close this tab.</p>
|
|
590
|
-
</div></body></html>`;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
function encodeOAuthState(csrfToken, relayUrl) {
|
|
594
|
-
const payload = JSON.stringify({ t: csrfToken, r: relayUrl });
|
|
595
|
-
return Buffer.from(payload).toString('base64url');
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
function decodeOAuthState(stateStr) {
|
|
599
|
-
try {
|
|
600
|
-
const payload = JSON.parse(Buffer.from(stateStr, 'base64url').toString());
|
|
601
|
-
return { csrfToken: payload.t, relayUrl: payload.r };
|
|
602
|
-
} catch (_) {
|
|
603
|
-
return { csrfToken: stateStr, relayUrl: null };
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
function geminiOAuthRelayPage(code, state, error) {
|
|
608
|
-
const stateData = decodeOAuthState(state || '');
|
|
609
|
-
const relayUrl = stateData.relayUrl || '';
|
|
610
|
-
const escapedCode = (code || '').replace(/['"\\]/g, '');
|
|
611
|
-
const escapedState = (state || '').replace(/['"\\]/g, '');
|
|
612
|
-
const escapedError = (error || '').replace(/['"\\]/g, '');
|
|
613
|
-
const escapedRelay = relayUrl.replace(/['"\\]/g, '');
|
|
614
|
-
return `<!DOCTYPE html><html><head><title>Completing sign-in...</title></head>
|
|
615
|
-
<body style="margin:0;display:flex;align-items:center;justify-content:center;min-height:100vh;background:#111827;font-family:system-ui,sans-serif;color:white;">
|
|
616
|
-
<div id="status" style="text-align:center;max-width:400px;padding:2rem;">
|
|
617
|
-
<div id="spinner" style="font-size:2rem;margin-bottom:1rem;">⌛</div>
|
|
618
|
-
<h1 id="title" style="font-size:1.5rem;margin-bottom:0.5rem;">Completing sign-in...</h1>
|
|
619
|
-
<p id="msg" style="color:#9ca3af;">Relaying authentication to server...</p>
|
|
620
|
-
</div>
|
|
621
|
-
<script>
|
|
622
|
-
(function() {
|
|
623
|
-
var code = '${escapedCode}';
|
|
624
|
-
var state = '${escapedState}';
|
|
625
|
-
var error = '${escapedError}';
|
|
626
|
-
var relayUrl = '${escapedRelay}';
|
|
627
|
-
function show(icon, title, msg, color) {
|
|
628
|
-
document.getElementById('spinner').textContent = icon;
|
|
629
|
-
document.getElementById('spinner').style.color = color;
|
|
630
|
-
document.getElementById('title').textContent = title;
|
|
631
|
-
document.getElementById('msg').textContent = msg;
|
|
632
|
-
}
|
|
633
|
-
if (error) { show('\\u2717', 'Authentication Failed', error, '#ef4444'); return; }
|
|
634
|
-
if (!code) { show('\\u2717', 'Authentication Failed', 'No authorization code received.', '#ef4444'); return; }
|
|
635
|
-
if (!relayUrl) { show('\\u2713', 'Authentication Successful', 'Credentials saved. You can close this tab.', '#10b981'); return; }
|
|
636
|
-
fetch(relayUrl, {
|
|
637
|
-
method: 'POST',
|
|
638
|
-
headers: { 'Content-Type': 'application/json' },
|
|
639
|
-
body: JSON.stringify({ code: code, state: state })
|
|
640
|
-
}).then(function(r) { return r.json(); }).then(function(data) {
|
|
641
|
-
if (data.success) {
|
|
642
|
-
show('\\u2713', 'Authentication Successful', data.email ? 'Signed in as ' + data.email + '. You can close this tab.' : 'Credentials saved. You can close this tab.', '#10b981');
|
|
643
|
-
} else {
|
|
644
|
-
show('\\u2717', 'Authentication Failed', data.error || 'Unknown error', '#ef4444');
|
|
645
|
-
}
|
|
646
|
-
}).catch(function(e) {
|
|
647
|
-
show('\\u2717', 'Relay Failed', 'Could not reach server: ' + e.message + '. You may need to paste the URL manually.', '#ef4444');
|
|
648
|
-
});
|
|
649
|
-
})();
|
|
650
|
-
</script>
|
|
651
|
-
</body></html>`;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
function isRemoteRequest(req) {
|
|
655
|
-
return !!(req && (req.headers['x-forwarded-for'] || req.headers['x-forwarded-host'] || req.headers['x-forwarded-proto']));
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
async function startGeminiOAuth(req) {
|
|
659
|
-
const creds = getGeminiOAuthCreds();
|
|
660
|
-
if (!creds) throw new Error('Could not find Gemini CLI OAuth credentials. Install gemini CLI first.');
|
|
661
|
-
|
|
662
|
-
const useCustomClient = !!creds.custom;
|
|
663
|
-
const remote = isRemoteRequest(req);
|
|
664
|
-
let redirectUri;
|
|
665
|
-
if (useCustomClient && req) {
|
|
666
|
-
redirectUri = `${buildBaseUrl(req)}${BASE_URL}/oauth2callback`;
|
|
667
|
-
} else {
|
|
668
|
-
redirectUri = `http://localhost:${PORT}${BASE_URL}/oauth2callback`;
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
const csrfToken = crypto.randomBytes(32).toString('hex');
|
|
672
|
-
const relayUrl = req ? `${buildBaseUrl(req)}${BASE_URL}/api/gemini-oauth/relay` : null;
|
|
673
|
-
const state = encodeOAuthState(csrfToken, relayUrl);
|
|
674
|
-
|
|
675
|
-
const client = new OAuth2Client({
|
|
676
|
-
clientId: creds.clientId,
|
|
677
|
-
clientSecret: creds.clientSecret,
|
|
678
|
-
});
|
|
679
|
-
|
|
680
|
-
const authUrl = client.generateAuthUrl({
|
|
681
|
-
redirect_uri: redirectUri,
|
|
682
|
-
access_type: 'offline',
|
|
683
|
-
scope: GEMINI_SCOPES,
|
|
684
|
-
state,
|
|
685
|
-
});
|
|
686
|
-
|
|
687
|
-
const mode = useCustomClient ? 'custom' : (remote ? 'cli-remote' : 'cli-local');
|
|
688
|
-
geminiOAuthPending = { client, redirectUri, state: csrfToken };
|
|
689
|
-
geminiOAuthState = { status: 'pending', error: null, email: null };
|
|
690
|
-
|
|
691
|
-
setTimeout(() => {
|
|
692
|
-
if (geminiOAuthState.status === 'pending') {
|
|
693
|
-
geminiOAuthState = { status: 'error', error: 'Authentication timed out', email: null };
|
|
694
|
-
geminiOAuthPending = null;
|
|
695
|
-
}
|
|
696
|
-
}, 5 * 60 * 1000);
|
|
697
|
-
|
|
698
|
-
return { authUrl, mode };
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
async function exchangeGeminiOAuthCode(code, stateParam) {
|
|
702
|
-
if (!geminiOAuthPending) throw new Error('No pending OAuth flow. Please start authentication again.');
|
|
703
|
-
|
|
704
|
-
const { client, redirectUri, state: expectedCsrf } = geminiOAuthPending;
|
|
705
|
-
const { csrfToken } = decodeOAuthState(stateParam);
|
|
706
|
-
|
|
707
|
-
if (csrfToken !== expectedCsrf) {
|
|
708
|
-
geminiOAuthState = { status: 'error', error: 'State mismatch', email: null };
|
|
709
|
-
geminiOAuthPending = null;
|
|
710
|
-
throw new Error('State mismatch - possible CSRF attack.');
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
if (!code) {
|
|
714
|
-
geminiOAuthState = { status: 'error', error: 'No authorization code received', email: null };
|
|
715
|
-
geminiOAuthPending = null;
|
|
716
|
-
throw new Error('No authorization code received.');
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
const { tokens } = await client.getToken({ code, redirect_uri: redirectUri });
|
|
720
|
-
client.setCredentials(tokens);
|
|
721
|
-
|
|
722
|
-
let email = '';
|
|
723
|
-
try {
|
|
724
|
-
const { token } = await client.getAccessToken();
|
|
725
|
-
if (token) {
|
|
726
|
-
const resp = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
|
|
727
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
728
|
-
});
|
|
729
|
-
if (resp.ok) {
|
|
730
|
-
const info = await resp.json();
|
|
731
|
-
email = info.email || '';
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
} catch (_) {}
|
|
735
|
-
|
|
736
|
-
saveGeminiCredentials(tokens, email);
|
|
737
|
-
geminiOAuthState = { status: 'success', error: null, email };
|
|
738
|
-
geminiOAuthPending = null;
|
|
739
|
-
|
|
740
|
-
return email;
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
async function handleGeminiOAuthCallback(req, res) {
|
|
744
|
-
const reqUrl = new URL(req.url, `http://localhost:${PORT}`);
|
|
745
|
-
const code = reqUrl.searchParams.get('code');
|
|
746
|
-
const state = reqUrl.searchParams.get('state');
|
|
747
|
-
const error = reqUrl.searchParams.get('error');
|
|
748
|
-
const errorDesc = reqUrl.searchParams.get('error_description');
|
|
749
|
-
|
|
750
|
-
if (error) {
|
|
751
|
-
const desc = errorDesc || error;
|
|
752
|
-
geminiOAuthState = { status: 'error', error: desc, email: null };
|
|
753
|
-
geminiOAuthPending = null;
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
const stateData = decodeOAuthState(state || '');
|
|
757
|
-
if (stateData.relayUrl) {
|
|
758
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
759
|
-
res.end(geminiOAuthRelayPage(code, state, errorDesc || error));
|
|
760
|
-
return;
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
if (!geminiOAuthPending) {
|
|
764
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
765
|
-
res.end(geminiOAuthResultPage('Authentication Failed', 'No pending OAuth flow.', false));
|
|
766
|
-
return;
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
try {
|
|
770
|
-
if (error) throw new Error(errorDesc || error);
|
|
771
|
-
const email = await exchangeGeminiOAuthCode(code, state);
|
|
772
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
773
|
-
res.end(geminiOAuthResultPage('Authentication Successful', email ? `Signed in as ${email}` : 'Gemini CLI credentials saved.', true));
|
|
774
|
-
} catch (e) {
|
|
775
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
776
|
-
res.end(geminiOAuthResultPage('Authentication Failed', e.message, false));
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
function getGeminiOAuthStatus() {
|
|
781
|
-
try {
|
|
782
|
-
if (fs.existsSync(GEMINI_OAUTH_FILE)) {
|
|
783
|
-
const creds = JSON.parse(fs.readFileSync(GEMINI_OAUTH_FILE, 'utf8'));
|
|
784
|
-
if (creds.refresh_token || creds.access_token) {
|
|
785
|
-
let email = '';
|
|
786
|
-
try {
|
|
787
|
-
if (fs.existsSync(GEMINI_ACCOUNTS_FILE)) {
|
|
788
|
-
const accts = JSON.parse(fs.readFileSync(GEMINI_ACCOUNTS_FILE, 'utf8'));
|
|
789
|
-
email = accts.active || '';
|
|
790
|
-
}
|
|
791
|
-
} catch (_) {}
|
|
792
|
-
return { hasKey: true, apiKey: email || '****oauth', defaultModel: '', path: GEMINI_OAUTH_FILE, authMethod: 'oauth' };
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
} catch (_) {}
|
|
796
|
-
return null;
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
const CODEX_HOME = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
800
|
-
const CODEX_AUTH_FILE = path.join(CODEX_HOME, 'auth.json');
|
|
801
|
-
const CODEX_OAUTH_ISSUER = 'https://auth.openai.com';
|
|
802
|
-
const CODEX_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
|
803
|
-
const CODEX_SCOPES = 'openid profile email offline_access api.connectors.read api.connectors.invoke';
|
|
804
|
-
const CODEX_OAUTH_PORT = 1455;
|
|
805
|
-
|
|
806
|
-
let codexOAuthState = { status: 'idle', error: null, email: null };
|
|
807
|
-
let codexOAuthPending = null;
|
|
808
|
-
|
|
809
|
-
function generatePkce() {
|
|
810
|
-
const verifierBytes = crypto.randomBytes(64);
|
|
811
|
-
const codeVerifier = verifierBytes.toString('base64url');
|
|
812
|
-
const challengeBytes = crypto.createHash('sha256').update(codeVerifier).digest();
|
|
813
|
-
const codeChallenge = challengeBytes.toString('base64url');
|
|
814
|
-
return { codeVerifier, codeChallenge };
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
function parseJwtEmail(jwt) {
|
|
818
|
-
try {
|
|
819
|
-
const parts = jwt.split('.');
|
|
820
|
-
if (parts.length < 2) return '';
|
|
821
|
-
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
|
822
|
-
return payload.email || payload['https://api.openai.com/profile']?.email || '';
|
|
823
|
-
} catch (_) { return ''; }
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
function saveCodexCredentials(tokens) {
|
|
827
|
-
if (!fs.existsSync(CODEX_HOME)) fs.mkdirSync(CODEX_HOME, { recursive: true });
|
|
828
|
-
const auth = { auth_mode: 'chatgpt', tokens, last_refresh: new Date().toISOString() };
|
|
829
|
-
fs.writeFileSync(CODEX_AUTH_FILE, JSON.stringify(auth, null, 2), { mode: 0o600 });
|
|
830
|
-
try { fs.chmodSync(CODEX_AUTH_FILE, 0o600); } catch (_) {}
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
function getCodexOAuthStatus() {
|
|
834
|
-
try {
|
|
835
|
-
if (fs.existsSync(CODEX_AUTH_FILE)) {
|
|
836
|
-
const auth = JSON.parse(fs.readFileSync(CODEX_AUTH_FILE, 'utf8'));
|
|
837
|
-
if (auth.tokens?.access_token || auth.tokens?.refresh_token) {
|
|
838
|
-
const email = parseJwtEmail(auth.tokens?.id_token || '') || '';
|
|
839
|
-
return { hasKey: true, apiKey: email || '****oauth', defaultModel: '', path: CODEX_AUTH_FILE, authMethod: 'oauth' };
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
} catch (_) {}
|
|
843
|
-
return null;
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
async function startCodexOAuth(req) {
|
|
847
|
-
const remote = isRemoteRequest(req);
|
|
848
|
-
const redirectUri = remote
|
|
849
|
-
? `${buildBaseUrl(req)}${BASE_URL}/codex-oauth2callback`
|
|
850
|
-
: `http://localhost:${CODEX_OAUTH_PORT}/auth/callback`;
|
|
851
|
-
|
|
852
|
-
const pkce = generatePkce();
|
|
853
|
-
const csrfToken = crypto.randomBytes(32).toString('hex');
|
|
854
|
-
const relayUrl = remote ? `${buildBaseUrl(req)}${BASE_URL}/api/codex-oauth/relay` : null;
|
|
855
|
-
const state = encodeOAuthState(csrfToken, relayUrl);
|
|
856
|
-
|
|
857
|
-
const params = new URLSearchParams({
|
|
858
|
-
response_type: 'code',
|
|
859
|
-
client_id: CODEX_CLIENT_ID,
|
|
860
|
-
redirect_uri: redirectUri,
|
|
861
|
-
scope: CODEX_SCOPES,
|
|
862
|
-
code_challenge: pkce.codeChallenge,
|
|
863
|
-
code_challenge_method: 'S256',
|
|
864
|
-
id_token_add_organizations: 'true',
|
|
865
|
-
codex_cli_simplified_flow: 'true',
|
|
866
|
-
state,
|
|
867
|
-
});
|
|
868
|
-
|
|
869
|
-
const authUrl = `${CODEX_OAUTH_ISSUER}/oauth/authorize?${params.toString()}`;
|
|
870
|
-
const mode = remote ? 'remote' : 'local';
|
|
871
|
-
|
|
872
|
-
codexOAuthPending = { pkce, redirectUri, state: csrfToken };
|
|
873
|
-
codexOAuthState = { status: 'pending', error: null, email: null };
|
|
874
|
-
|
|
875
|
-
setTimeout(() => {
|
|
876
|
-
if (codexOAuthState.status === 'pending') {
|
|
877
|
-
codexOAuthState = { status: 'error', error: 'Authentication timed out', email: null };
|
|
878
|
-
codexOAuthPending = null;
|
|
879
|
-
}
|
|
880
|
-
}, 5 * 60 * 1000);
|
|
881
|
-
|
|
882
|
-
return { authUrl, mode };
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
async function exchangeCodexOAuthCode(code, stateParam) {
|
|
886
|
-
if (!codexOAuthPending) throw new Error('No pending OAuth flow. Please start authentication again.');
|
|
887
|
-
|
|
888
|
-
const { pkce, redirectUri, state: expectedCsrf } = codexOAuthPending;
|
|
889
|
-
const { csrfToken } = decodeOAuthState(stateParam);
|
|
890
|
-
|
|
891
|
-
if (csrfToken !== expectedCsrf) {
|
|
892
|
-
codexOAuthState = { status: 'error', error: 'State mismatch', email: null };
|
|
893
|
-
codexOAuthPending = null;
|
|
894
|
-
throw new Error('State mismatch - possible CSRF attack.');
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
if (!code) {
|
|
898
|
-
codexOAuthState = { status: 'error', error: 'No authorization code received', email: null };
|
|
899
|
-
codexOAuthPending = null;
|
|
900
|
-
throw new Error('No authorization code received.');
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
const body = new URLSearchParams({
|
|
904
|
-
grant_type: 'authorization_code',
|
|
905
|
-
code,
|
|
906
|
-
redirect_uri: redirectUri,
|
|
907
|
-
client_id: CODEX_CLIENT_ID,
|
|
908
|
-
code_verifier: pkce.codeVerifier,
|
|
909
|
-
});
|
|
910
|
-
|
|
911
|
-
const resp = await fetch(`${CODEX_OAUTH_ISSUER}/oauth/token`, {
|
|
912
|
-
method: 'POST',
|
|
913
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
914
|
-
body: body.toString(),
|
|
915
|
-
});
|
|
916
|
-
|
|
917
|
-
if (!resp.ok) {
|
|
918
|
-
const text = await resp.text();
|
|
919
|
-
codexOAuthState = { status: 'error', error: `Token exchange failed: ${resp.status}`, email: null };
|
|
920
|
-
codexOAuthPending = null;
|
|
921
|
-
throw new Error(`Token exchange failed (${resp.status}): ${text}`);
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
const tokens = await resp.json();
|
|
925
|
-
const email = parseJwtEmail(tokens.id_token || '');
|
|
926
|
-
|
|
927
|
-
saveCodexCredentials(tokens);
|
|
928
|
-
codexOAuthState = { status: 'success', error: null, email };
|
|
929
|
-
codexOAuthPending = null;
|
|
930
|
-
|
|
931
|
-
return email;
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
function codexOAuthResultPage(title, message, success) {
|
|
935
|
-
const color = success ? '#10b981' : '#ef4444';
|
|
936
|
-
const icon = success ? '✓' : '✗';
|
|
937
|
-
return `<!DOCTYPE html><html><head><title>${title}</title></head>
|
|
938
|
-
<body style="margin:0;display:flex;align-items:center;justify-content:center;min-height:100vh;background:#111827;font-family:system-ui,sans-serif;color:white;">
|
|
939
|
-
<div style="text-align:center;max-width:400px;padding:2rem;">
|
|
940
|
-
<div style="font-size:4rem;color:${color};margin-bottom:1rem;">${icon}</div>
|
|
941
|
-
<h1 style="font-size:1.5rem;margin-bottom:0.5rem;">${title}</h1>
|
|
942
|
-
<p style="color:#9ca3af;">${message}</p>
|
|
943
|
-
<p style="color:#6b7280;margin-top:1rem;font-size:0.875rem;">You can close this tab.</p>
|
|
944
|
-
</div></body></html>`;
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
function codexOAuthRelayPage(code, state, error) {
|
|
948
|
-
const stateData = decodeOAuthState(state || '');
|
|
949
|
-
const relayUrl = stateData.relayUrl || '';
|
|
950
|
-
const escapedCode = (code || '').replace(/['"\\]/g, '');
|
|
951
|
-
const escapedState = (state || '').replace(/['"\\]/g, '');
|
|
952
|
-
const escapedError = (error || '').replace(/['"\\]/g, '');
|
|
953
|
-
const escapedRelay = relayUrl.replace(/['"\\]/g, '');
|
|
954
|
-
return `<!DOCTYPE html><html><head><title>Completing sign-in...</title></head>
|
|
955
|
-
<body style="margin:0;display:flex;align-items:center;justify-content:center;min-height:100vh;background:#111827;font-family:system-ui,sans-serif;color:white;">
|
|
956
|
-
<div id="status" style="text-align:center;max-width:400px;padding:2rem;">
|
|
957
|
-
<div id="spinner" style="font-size:2rem;margin-bottom:1rem;">⌛</div>
|
|
958
|
-
<h1 id="title" style="font-size:1.5rem;margin-bottom:0.5rem;">Completing sign-in...</h1>
|
|
959
|
-
<p id="msg" style="color:#9ca3af;">Relaying authentication to server...</p>
|
|
960
|
-
</div>
|
|
961
|
-
<script>
|
|
962
|
-
(function() {
|
|
963
|
-
var code = '${escapedCode}';
|
|
964
|
-
var state = '${escapedState}';
|
|
965
|
-
var error = '${escapedError}';
|
|
966
|
-
var relayUrl = '${escapedRelay}';
|
|
967
|
-
function show(icon, title, msg, color) {
|
|
968
|
-
document.getElementById('spinner').textContent = icon;
|
|
969
|
-
document.getElementById('spinner').style.color = color;
|
|
970
|
-
document.getElementById('title').textContent = title;
|
|
971
|
-
document.getElementById('msg').textContent = msg;
|
|
972
|
-
}
|
|
973
|
-
if (error) { show('\\u2717', 'Authentication Failed', error, '#ef4444'); return; }
|
|
974
|
-
if (!code) { show('\\u2717', 'Authentication Failed', 'No authorization code received.', '#ef4444'); return; }
|
|
975
|
-
if (!relayUrl) { show('\\u2713', 'Authentication Successful', 'Credentials saved. You can close this tab.', '#10b981'); return; }
|
|
976
|
-
fetch(relayUrl, {
|
|
977
|
-
method: 'POST',
|
|
978
|
-
headers: { 'Content-Type': 'application/json' },
|
|
979
|
-
body: JSON.stringify({ code: code, state: state })
|
|
980
|
-
}).then(function(r) { return r.json(); }).then(function(data) {
|
|
981
|
-
if (data.success) {
|
|
982
|
-
show('\\u2713', 'Authentication Successful', data.email ? 'Signed in as ' + data.email + '. You can close this tab.' : 'Credentials saved. You can close this tab.', '#10b981');
|
|
983
|
-
} else {
|
|
984
|
-
show('\\u2717', 'Authentication Failed', data.error || 'Unknown error', '#ef4444');
|
|
985
|
-
}
|
|
986
|
-
}).catch(function(e) {
|
|
987
|
-
show('\\u2717', 'Relay Failed', 'Could not reach server: ' + e.message + '. You may need to paste the URL manually.', '#ef4444');
|
|
988
|
-
});
|
|
989
|
-
})();
|
|
990
|
-
</script>
|
|
991
|
-
</body></html>`;
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
async function handleCodexOAuthCallback(req, res) {
|
|
995
|
-
const reqUrl = new URL(req.url, `http://localhost:${PORT}`);
|
|
996
|
-
const code = reqUrl.searchParams.get('code');
|
|
997
|
-
const state = reqUrl.searchParams.get('state');
|
|
998
|
-
const error = reqUrl.searchParams.get('error');
|
|
999
|
-
const errorDesc = reqUrl.searchParams.get('error_description');
|
|
1000
|
-
|
|
1001
|
-
if (error) {
|
|
1002
|
-
const desc = errorDesc || error;
|
|
1003
|
-
codexOAuthState = { status: 'error', error: desc, email: null };
|
|
1004
|
-
codexOAuthPending = null;
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
const stateData = decodeOAuthState(state || '');
|
|
1008
|
-
if (stateData.relayUrl) {
|
|
1009
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
1010
|
-
res.end(codexOAuthRelayPage(code, state, errorDesc || error));
|
|
1011
|
-
return;
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
if (!codexOAuthPending) {
|
|
1015
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
1016
|
-
res.end(codexOAuthResultPage('Authentication Failed', 'No pending OAuth flow.', false));
|
|
1017
|
-
return;
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
try {
|
|
1021
|
-
if (error) throw new Error(errorDesc || error);
|
|
1022
|
-
const email = await exchangeCodexOAuthCode(code, state);
|
|
1023
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
1024
|
-
res.end(codexOAuthResultPage('Authentication Successful', email ? `Signed in as ${email}` : 'Codex CLI credentials saved.', true));
|
|
1025
|
-
} catch (e) {
|
|
1026
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
1027
|
-
res.end(codexOAuthResultPage('Authentication Failed', e.message, false));
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
262
|
const PROVIDER_CONFIGS = {
|
|
1032
263
|
'anthropic': {
|
|
1033
264
|
name: 'Anthropic', configPaths: [
|
|
@@ -1281,12 +512,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
1281
512
|
const pathOnly = routePath.split('?')[0];
|
|
1282
513
|
|
|
1283
514
|
if (pathOnly === '/oauth2callback' && req.method === 'GET') {
|
|
1284
|
-
await handleGeminiOAuthCallback(req, res);
|
|
515
|
+
await handleGeminiOAuthCallback(req, res, PORT);
|
|
1285
516
|
return;
|
|
1286
517
|
}
|
|
1287
518
|
|
|
1288
519
|
if (pathOnly === '/codex-oauth2callback' && req.method === 'GET') {
|
|
1289
|
-
await handleCodexOAuthCallback(req, res);
|
|
520
|
+
await handleCodexOAuthCallback(req, res, PORT);
|
|
1290
521
|
return;
|
|
1291
522
|
}
|
|
1292
523
|
|
|
@@ -2746,7 +1977,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
2746
1977
|
|
|
2747
1978
|
if (pathOnly === '/api/gemini-oauth/start' && req.method === 'POST') {
|
|
2748
1979
|
try {
|
|
2749
|
-
const result = await startGeminiOAuth(req);
|
|
1980
|
+
const result = await startGeminiOAuth(req, { PORT, BASE_URL, rootDir });
|
|
2750
1981
|
sendJSON(req, res, 200, { authUrl: result.authUrl, mode: result.mode });
|
|
2751
1982
|
} catch (e) {
|
|
2752
1983
|
console.error('[gemini-oauth] /api/gemini-oauth/start failed:', e);
|
|
@@ -2756,7 +1987,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
2756
1987
|
}
|
|
2757
1988
|
|
|
2758
1989
|
if (pathOnly === '/api/gemini-oauth/status' && req.method === 'GET') {
|
|
2759
|
-
sendJSON(req, res, 200,
|
|
1990
|
+
sendJSON(req, res, 200, getGeminiOAuthState());
|
|
2760
1991
|
return;
|
|
2761
1992
|
}
|
|
2762
1993
|
|
|
@@ -2771,8 +2002,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
2771
2002
|
const email = await exchangeGeminiOAuthCode(code, stateParam);
|
|
2772
2003
|
sendJSON(req, res, 200, { success: true, email });
|
|
2773
2004
|
} catch (e) {
|
|
2774
|
-
geminiOAuthState = { status: 'error', error: e.message, email: null };
|
|
2775
|
-
geminiOAuthPending = null;
|
|
2776
2005
|
sendJSON(req, res, 400, { error: e.message });
|
|
2777
2006
|
}
|
|
2778
2007
|
return;
|
|
@@ -2796,8 +2025,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
2796
2025
|
const error = parsed.searchParams.get('error');
|
|
2797
2026
|
if (error) {
|
|
2798
2027
|
const desc = parsed.searchParams.get('error_description') || error;
|
|
2799
|
-
geminiOAuthState = { status: 'error', error: desc, email: null };
|
|
2800
|
-
geminiOAuthPending = null;
|
|
2801
2028
|
sendJSON(req, res, 200, { error: desc });
|
|
2802
2029
|
return;
|
|
2803
2030
|
}
|
|
@@ -2807,8 +2034,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
2807
2034
|
const email = await exchangeGeminiOAuthCode(code, state);
|
|
2808
2035
|
sendJSON(req, res, 200, { success: true, email });
|
|
2809
2036
|
} catch (e) {
|
|
2810
|
-
geminiOAuthState = { status: 'error', error: e.message, email: null };
|
|
2811
|
-
geminiOAuthPending = null;
|
|
2812
2037
|
sendJSON(req, res, 400, { error: e.message });
|
|
2813
2038
|
}
|
|
2814
2039
|
return;
|
|
@@ -2816,7 +2041,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
2816
2041
|
|
|
2817
2042
|
if (pathOnly === '/api/codex-oauth/start' && req.method === 'POST') {
|
|
2818
2043
|
try {
|
|
2819
|
-
const result = await startCodexOAuth(req);
|
|
2044
|
+
const result = await startCodexOAuth(req, { PORT, BASE_URL });
|
|
2820
2045
|
sendJSON(req, res, 200, { authUrl: result.authUrl, mode: result.mode });
|
|
2821
2046
|
} catch (e) {
|
|
2822
2047
|
console.error('[codex-oauth] /api/codex-oauth/start failed:', e);
|
|
@@ -2826,7 +2051,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
2826
2051
|
}
|
|
2827
2052
|
|
|
2828
2053
|
if (pathOnly === '/api/codex-oauth/status' && req.method === 'GET') {
|
|
2829
|
-
sendJSON(req, res, 200,
|
|
2054
|
+
sendJSON(req, res, 200, getCodexOAuthState());
|
|
2830
2055
|
return;
|
|
2831
2056
|
}
|
|
2832
2057
|
|
|
@@ -2841,8 +2066,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
2841
2066
|
const email = await exchangeCodexOAuthCode(code, stateParam);
|
|
2842
2067
|
sendJSON(req, res, 200, { success: true, email });
|
|
2843
2068
|
} catch (e) {
|
|
2844
|
-
codexOAuthState = { status: 'error', error: e.message, email: null };
|
|
2845
|
-
codexOAuthPending = null;
|
|
2846
2069
|
sendJSON(req, res, 400, { error: e.message });
|
|
2847
2070
|
}
|
|
2848
2071
|
return;
|
|
@@ -2864,8 +2087,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
2864
2087
|
const error = parsed.searchParams.get('error');
|
|
2865
2088
|
if (error) {
|
|
2866
2089
|
const desc = parsed.searchParams.get('error_description') || error;
|
|
2867
|
-
codexOAuthState = { status: 'error', error: desc, email: null };
|
|
2868
|
-
codexOAuthPending = null;
|
|
2869
2090
|
sendJSON(req, res, 200, { error: desc });
|
|
2870
2091
|
return;
|
|
2871
2092
|
}
|
|
@@ -2874,8 +2095,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
2874
2095
|
const email = await exchangeCodexOAuthCode(code, state);
|
|
2875
2096
|
sendJSON(req, res, 200, { success: true, email });
|
|
2876
2097
|
} catch (e) {
|
|
2877
|
-
codexOAuthState = { status: 'error', error: e.message, email: null };
|
|
2878
|
-
codexOAuthPending = null;
|
|
2879
2098
|
sendJSON(req, res, 400, { error: e.message });
|
|
2880
2099
|
}
|
|
2881
2100
|
return;
|
|
@@ -2889,21 +2108,21 @@ const server = http.createServer(async (req, res) => {
|
|
|
2889
2108
|
|
|
2890
2109
|
if (agentId === 'codex' || agentId === 'cli-codex') {
|
|
2891
2110
|
try {
|
|
2892
|
-
const result = await startCodexOAuth(req);
|
|
2111
|
+
const result = await startCodexOAuth(req, { PORT, BASE_URL });
|
|
2893
2112
|
const conversationId = '__agent_auth__';
|
|
2894
2113
|
broadcastSync({ type: 'script_started', conversationId, script: 'auth-codex', agentId: 'codex', timestamp: Date.now() });
|
|
2895
2114
|
broadcastSync({ type: 'script_output', conversationId, data: `\x1b[36mOpening OpenAI OAuth in your browser...\x1b[0m\r\n\r\nIf it doesn't open automatically, visit:\r\n${result.authUrl}\r\n`, stream: 'stdout', timestamp: Date.now() });
|
|
2896
2115
|
|
|
2897
2116
|
const pollId = setInterval(() => {
|
|
2898
|
-
if (
|
|
2117
|
+
if (getCodexOAuthState().status === 'success') {
|
|
2899
2118
|
clearInterval(pollId);
|
|
2900
|
-
const email =
|
|
2119
|
+
const email = getCodexOAuthState().email || '';
|
|
2901
2120
|
broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[32mAuthentication successful${email ? ' (' + email + ')' : ''}\x1b[0m\r\n`, stream: 'stdout', timestamp: Date.now() });
|
|
2902
2121
|
broadcastSync({ type: 'script_stopped', conversationId, code: 0, timestamp: Date.now() });
|
|
2903
|
-
} else if (
|
|
2122
|
+
} else if (getCodexOAuthState().status === 'error') {
|
|
2904
2123
|
clearInterval(pollId);
|
|
2905
|
-
broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[31mAuthentication failed: ${
|
|
2906
|
-
broadcastSync({ type: 'script_stopped', conversationId, code: 1, error:
|
|
2124
|
+
broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[31mAuthentication failed: ${getCodexOAuthState().error}\x1b[0m\r\n`, stream: 'stderr', timestamp: Date.now() });
|
|
2125
|
+
broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: getCodexOAuthState().error, timestamp: Date.now() });
|
|
2907
2126
|
}
|
|
2908
2127
|
}, 1000);
|
|
2909
2128
|
|
|
@@ -2920,21 +2139,21 @@ const server = http.createServer(async (req, res) => {
|
|
|
2920
2139
|
|
|
2921
2140
|
if (agentId === 'gemini') {
|
|
2922
2141
|
try {
|
|
2923
|
-
const result = await startGeminiOAuth(req);
|
|
2142
|
+
const result = await startGeminiOAuth(req, { PORT, BASE_URL, rootDir });
|
|
2924
2143
|
const conversationId = '__agent_auth__';
|
|
2925
2144
|
broadcastSync({ type: 'script_started', conversationId, script: 'auth-gemini', agentId: 'gemini', timestamp: Date.now() });
|
|
2926
2145
|
broadcastSync({ type: 'script_output', conversationId, data: `\x1b[36mOpening Google OAuth in your browser...\x1b[0m\r\n\r\nIf it doesn't open automatically, visit:\r\n${result.authUrl}\r\n`, stream: 'stdout', timestamp: Date.now() });
|
|
2927
2146
|
|
|
2928
2147
|
const pollId = setInterval(() => {
|
|
2929
|
-
if (
|
|
2148
|
+
if (getGeminiOAuthState().status === 'success') {
|
|
2930
2149
|
clearInterval(pollId);
|
|
2931
|
-
const email =
|
|
2150
|
+
const email = getGeminiOAuthState().email || '';
|
|
2932
2151
|
broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[32mAuthentication successful${email ? ' (' + email + ')' : ''}\x1b[0m\r\n`, stream: 'stdout', timestamp: Date.now() });
|
|
2933
2152
|
broadcastSync({ type: 'script_stopped', conversationId, code: 0, timestamp: Date.now() });
|
|
2934
|
-
} else if (
|
|
2153
|
+
} else if (getGeminiOAuthState().status === 'error') {
|
|
2935
2154
|
clearInterval(pollId);
|
|
2936
|
-
broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[31mAuthentication failed: ${
|
|
2937
|
-
broadcastSync({ type: 'script_stopped', conversationId, code: 1, error:
|
|
2155
|
+
broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[31mAuthentication failed: ${getGeminiOAuthState().error}\x1b[0m\r\n`, stream: 'stderr', timestamp: Date.now() });
|
|
2156
|
+
broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: getGeminiOAuthState().error, timestamp: Date.now() });
|
|
2938
2157
|
}
|
|
2939
2158
|
}, 1000);
|
|
2940
2159
|
|
|
@@ -4499,6 +3718,8 @@ function broadcastSync(event) {
|
|
|
4499
3718
|
// WebSocket protocol router
|
|
4500
3719
|
const wsRouter = new WsRouter();
|
|
4501
3720
|
|
|
3721
|
+
initSpeechManager({ broadcastSync, syncClients, queries });
|
|
3722
|
+
|
|
4502
3723
|
registerConvHandlers(wsRouter, {
|
|
4503
3724
|
queries, activeExecutions, rateLimitState,
|
|
4504
3725
|
broadcastSync, processMessageWithStreaming, cleanupExecution,
|
|
@@ -4516,7 +3737,7 @@ console.log('[INIT] About to call registerSessionHandlers, discoveredAgents.leng
|
|
|
4516
3737
|
registerSessionHandlers(wsRouter, {
|
|
4517
3738
|
db: queries, discoveredAgents, modelCache,
|
|
4518
3739
|
getAgentDescriptor, activeScripts, broadcastSync,
|
|
4519
|
-
startGeminiOAuth
|
|
3740
|
+
startGeminiOAuth: (req) => startGeminiOAuth(req, { PORT, BASE_URL, rootDir }), geminiOAuthState: getGeminiOAuthState
|
|
4520
3741
|
});
|
|
4521
3742
|
console.log('[INIT] registerSessionHandlers completed');
|
|
4522
3743
|
|
|
@@ -4536,10 +3757,12 @@ registerScriptHandlers(wsRouter, {
|
|
|
4536
3757
|
});
|
|
4537
3758
|
|
|
4538
3759
|
registerOAuthHandlers(wsRouter, {
|
|
4539
|
-
startGeminiOAuth,
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
3760
|
+
startGeminiOAuth: (req) => startGeminiOAuth(req, { PORT, BASE_URL, rootDir }),
|
|
3761
|
+
exchangeGeminiOAuthCode,
|
|
3762
|
+
geminiOAuthState: getGeminiOAuthState,
|
|
3763
|
+
startCodexOAuth: (req) => startCodexOAuth(req, { PORT, BASE_URL }),
|
|
3764
|
+
exchangeCodexOAuthCode,
|
|
3765
|
+
codexOAuthState: getCodexOAuthState,
|
|
4543
3766
|
});
|
|
4544
3767
|
|
|
4545
3768
|
wsRouter.onLegacy((data, ws) => {
|