agentgui 1.0.752 → 1.0.753
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/package.json +1 -1
- package/server.js +27 -581
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import crypto from 'crypto';
|
|
5
|
+
import { buildBaseUrl, isRemoteRequest, encodeOAuthState, decodeOAuthState, oauthResultPage, oauthRelayPage } from './oauth-common.js';
|
|
6
|
+
|
|
7
|
+
const CODEX_HOME = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
8
|
+
const CODEX_AUTH_FILE = path.join(CODEX_HOME, 'auth.json');
|
|
9
|
+
const CODEX_OAUTH_ISSUER = 'https://auth.openai.com';
|
|
10
|
+
const CODEX_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
|
11
|
+
const CODEX_SCOPES = 'openid profile email offline_access api.connectors.read api.connectors.invoke';
|
|
12
|
+
const CODEX_OAUTH_PORT = 1455;
|
|
13
|
+
|
|
14
|
+
let codexOAuthState = { status: 'idle', error: null, email: null };
|
|
15
|
+
let codexOAuthPending = null;
|
|
16
|
+
|
|
17
|
+
function generatePkce() {
|
|
18
|
+
const verifierBytes = crypto.randomBytes(64);
|
|
19
|
+
const codeVerifier = verifierBytes.toString('base64url');
|
|
20
|
+
const challengeBytes = crypto.createHash('sha256').update(codeVerifier).digest();
|
|
21
|
+
const codeChallenge = challengeBytes.toString('base64url');
|
|
22
|
+
return { codeVerifier, codeChallenge };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function parseJwtEmail(jwt) {
|
|
26
|
+
try {
|
|
27
|
+
const parts = jwt.split('.');
|
|
28
|
+
if (parts.length < 2) return '';
|
|
29
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
|
30
|
+
return payload.email || payload['https://api.openai.com/profile']?.email || '';
|
|
31
|
+
} catch (_) { return ''; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function saveCodexCredentials(tokens) {
|
|
35
|
+
if (!fs.existsSync(CODEX_HOME)) fs.mkdirSync(CODEX_HOME, { recursive: true });
|
|
36
|
+
const auth = { auth_mode: 'chatgpt', tokens, last_refresh: new Date().toISOString() };
|
|
37
|
+
fs.writeFileSync(CODEX_AUTH_FILE, JSON.stringify(auth, null, 2), { mode: 0o600 });
|
|
38
|
+
try { fs.chmodSync(CODEX_AUTH_FILE, 0o600); } catch (_) {}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getCodexOAuthStatus() {
|
|
42
|
+
try {
|
|
43
|
+
if (fs.existsSync(CODEX_AUTH_FILE)) {
|
|
44
|
+
const auth = JSON.parse(fs.readFileSync(CODEX_AUTH_FILE, 'utf8'));
|
|
45
|
+
if (auth.tokens?.access_token || auth.tokens?.refresh_token) {
|
|
46
|
+
const email = parseJwtEmail(auth.tokens?.id_token || '') || '';
|
|
47
|
+
return { hasKey: true, apiKey: email || '****oauth', defaultModel: '', path: CODEX_AUTH_FILE, authMethod: 'oauth' };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
} catch (_) {}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function startCodexOAuth(req, { PORT, BASE_URL }) {
|
|
55
|
+
const remote = isRemoteRequest(req);
|
|
56
|
+
const redirectUri = remote
|
|
57
|
+
? `${buildBaseUrl(req, PORT)}${BASE_URL}/codex-oauth2callback`
|
|
58
|
+
: `http://localhost:${CODEX_OAUTH_PORT}/auth/callback`;
|
|
59
|
+
const pkce = generatePkce();
|
|
60
|
+
const csrfToken = crypto.randomBytes(32).toString('hex');
|
|
61
|
+
const relayUrl = remote ? `${buildBaseUrl(req, PORT)}${BASE_URL}/api/codex-oauth/relay` : null;
|
|
62
|
+
const state = encodeOAuthState(csrfToken, relayUrl);
|
|
63
|
+
const params = new URLSearchParams({
|
|
64
|
+
response_type: 'code',
|
|
65
|
+
client_id: CODEX_CLIENT_ID,
|
|
66
|
+
redirect_uri: redirectUri,
|
|
67
|
+
scope: CODEX_SCOPES,
|
|
68
|
+
code_challenge: pkce.codeChallenge,
|
|
69
|
+
code_challenge_method: 'S256',
|
|
70
|
+
id_token_add_organizations: 'true',
|
|
71
|
+
codex_cli_simplified_flow: 'true',
|
|
72
|
+
state,
|
|
73
|
+
});
|
|
74
|
+
const authUrl = `${CODEX_OAUTH_ISSUER}/oauth/authorize?${params.toString()}`;
|
|
75
|
+
const mode = remote ? 'remote' : 'local';
|
|
76
|
+
codexOAuthPending = { pkce, redirectUri, state: csrfToken };
|
|
77
|
+
codexOAuthState = { status: 'pending', error: null, email: null };
|
|
78
|
+
setTimeout(() => {
|
|
79
|
+
if (codexOAuthState.status === 'pending') {
|
|
80
|
+
codexOAuthState = { status: 'error', error: 'Authentication timed out', email: null };
|
|
81
|
+
codexOAuthPending = null;
|
|
82
|
+
}
|
|
83
|
+
}, 5 * 60 * 1000);
|
|
84
|
+
return { authUrl, mode };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function exchangeCodexOAuthCode(code, stateParam) {
|
|
88
|
+
if (!codexOAuthPending) throw new Error('No pending OAuth flow. Please start authentication again.');
|
|
89
|
+
const { pkce, redirectUri, state: expectedCsrf } = codexOAuthPending;
|
|
90
|
+
const { csrfToken } = decodeOAuthState(stateParam);
|
|
91
|
+
if (csrfToken !== expectedCsrf) {
|
|
92
|
+
codexOAuthState = { status: 'error', error: 'State mismatch', email: null };
|
|
93
|
+
codexOAuthPending = null;
|
|
94
|
+
throw new Error('State mismatch - possible CSRF attack.');
|
|
95
|
+
}
|
|
96
|
+
if (!code) {
|
|
97
|
+
codexOAuthState = { status: 'error', error: 'No authorization code received', email: null };
|
|
98
|
+
codexOAuthPending = null;
|
|
99
|
+
throw new Error('No authorization code received.');
|
|
100
|
+
}
|
|
101
|
+
const body = new URLSearchParams({
|
|
102
|
+
grant_type: 'authorization_code',
|
|
103
|
+
code,
|
|
104
|
+
redirect_uri: redirectUri,
|
|
105
|
+
client_id: CODEX_CLIENT_ID,
|
|
106
|
+
code_verifier: pkce.codeVerifier,
|
|
107
|
+
});
|
|
108
|
+
const resp = await fetch(`${CODEX_OAUTH_ISSUER}/oauth/token`, {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
111
|
+
body: body.toString(),
|
|
112
|
+
});
|
|
113
|
+
if (!resp.ok) {
|
|
114
|
+
const text = await resp.text();
|
|
115
|
+
codexOAuthState = { status: 'error', error: `Token exchange failed: ${resp.status}`, email: null };
|
|
116
|
+
codexOAuthPending = null;
|
|
117
|
+
throw new Error(`Token exchange failed (${resp.status}): ${text}`);
|
|
118
|
+
}
|
|
119
|
+
const tokens = await resp.json();
|
|
120
|
+
const email = parseJwtEmail(tokens.id_token || '');
|
|
121
|
+
saveCodexCredentials(tokens);
|
|
122
|
+
codexOAuthState = { status: 'success', error: null, email };
|
|
123
|
+
codexOAuthPending = null;
|
|
124
|
+
return email;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function handleCodexOAuthCallback(req, res, PORT) {
|
|
128
|
+
const reqUrl = new URL(req.url, `http://localhost:${PORT}`);
|
|
129
|
+
const code = reqUrl.searchParams.get('code');
|
|
130
|
+
const state = reqUrl.searchParams.get('state');
|
|
131
|
+
const error = reqUrl.searchParams.get('error');
|
|
132
|
+
const errorDesc = reqUrl.searchParams.get('error_description');
|
|
133
|
+
if (error) {
|
|
134
|
+
const desc = errorDesc || error;
|
|
135
|
+
codexOAuthState = { status: 'error', error: desc, email: null };
|
|
136
|
+
codexOAuthPending = null;
|
|
137
|
+
}
|
|
138
|
+
const stateData = decodeOAuthState(state || '');
|
|
139
|
+
if (stateData.relayUrl) {
|
|
140
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
141
|
+
res.end(oauthRelayPage(code, state, errorDesc || error));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (!codexOAuthPending) {
|
|
145
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
146
|
+
res.end(oauthResultPage('Authentication Failed', 'No pending OAuth flow.', false));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
if (error) throw new Error(errorDesc || error);
|
|
151
|
+
const email = await exchangeCodexOAuthCode(code, state);
|
|
152
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
153
|
+
res.end(oauthResultPage('Authentication Successful', email ? `Signed in as ${email}` : 'Codex CLI credentials saved.', true));
|
|
154
|
+
} catch (e) {
|
|
155
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
156
|
+
res.end(oauthResultPage('Authentication Failed', e.message, false));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function getCodexOAuthState() { return codexOAuthState; }
|
|
161
|
+
export function getCodexOAuthPending() { return codexOAuthPending; }
|
|
162
|
+
export { CODEX_HOME, CODEX_AUTH_FILE };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
|
|
3
|
+
export function buildBaseUrl(req, PORT) {
|
|
4
|
+
const override = process.env.AGENTGUI_BASE_URL;
|
|
5
|
+
if (override) return override.replace(/\/+$/, '');
|
|
6
|
+
const fwdProto = req.headers['x-forwarded-proto'];
|
|
7
|
+
const fwdHost = req.headers['x-forwarded-host'] || req.headers['host'];
|
|
8
|
+
if (fwdHost) {
|
|
9
|
+
const proto = fwdProto || (req.socket.encrypted ? 'https' : 'http');
|
|
10
|
+
const cleanHost = fwdHost.replace(/:443$/, '').replace(/:80$/, '');
|
|
11
|
+
return `${proto}://${cleanHost}`;
|
|
12
|
+
}
|
|
13
|
+
return `http://127.0.0.1:${PORT}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isRemoteRequest(req) {
|
|
17
|
+
return !!(req && (req.headers['x-forwarded-for'] || req.headers['x-forwarded-host'] || req.headers['x-forwarded-proto']));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function encodeOAuthState(csrfToken, relayUrl) {
|
|
21
|
+
const payload = JSON.stringify({ t: csrfToken, r: relayUrl });
|
|
22
|
+
return Buffer.from(payload).toString('base64url');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function decodeOAuthState(stateStr) {
|
|
26
|
+
try {
|
|
27
|
+
const payload = JSON.parse(Buffer.from(stateStr, 'base64url').toString());
|
|
28
|
+
return { csrfToken: payload.t, relayUrl: payload.r };
|
|
29
|
+
} catch (_) {
|
|
30
|
+
return { csrfToken: stateStr, relayUrl: null };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function oauthResultPage(title, message, success) {
|
|
35
|
+
const color = success ? '#10b981' : '#ef4444';
|
|
36
|
+
const icon = success ? '✓' : '✗';
|
|
37
|
+
return `<!DOCTYPE html><html><head><title>${title}</title></head>
|
|
38
|
+
<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;">
|
|
39
|
+
<div style="text-align:center;max-width:400px;padding:2rem;">
|
|
40
|
+
<div style="font-size:4rem;color:${color};margin-bottom:1rem;">${icon}</div>
|
|
41
|
+
<h1 style="font-size:1.5rem;margin-bottom:0.5rem;">${title}</h1>
|
|
42
|
+
<p style="color:#9ca3af;">${message}</p>
|
|
43
|
+
<p style="color:#6b7280;margin-top:1rem;font-size:0.875rem;">You can close this tab.</p>
|
|
44
|
+
</div></body></html>`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function oauthRelayPage(code, state, error) {
|
|
48
|
+
const stateData = decodeOAuthState(state || '');
|
|
49
|
+
const relayUrl = stateData.relayUrl || '';
|
|
50
|
+
const escapedCode = (code || '').replace(/['"\\]/g, '');
|
|
51
|
+
const escapedState = (state || '').replace(/['"\\]/g, '');
|
|
52
|
+
const escapedError = (error || '').replace(/['"\\]/g, '');
|
|
53
|
+
const escapedRelay = relayUrl.replace(/['"\\]/g, '');
|
|
54
|
+
return `<!DOCTYPE html><html><head><title>Completing sign-in...</title></head>
|
|
55
|
+
<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;">
|
|
56
|
+
<div id="status" style="text-align:center;max-width:400px;padding:2rem;">
|
|
57
|
+
<div id="spinner" style="font-size:2rem;margin-bottom:1rem;">⌛</div>
|
|
58
|
+
<h1 id="title" style="font-size:1.5rem;margin-bottom:0.5rem;">Completing sign-in...</h1>
|
|
59
|
+
<p id="msg" style="color:#9ca3af;">Relaying authentication to server...</p>
|
|
60
|
+
</div>
|
|
61
|
+
<script>
|
|
62
|
+
(function() {
|
|
63
|
+
var code = '${escapedCode}';
|
|
64
|
+
var state = '${escapedState}';
|
|
65
|
+
var error = '${escapedError}';
|
|
66
|
+
var relayUrl = '${escapedRelay}';
|
|
67
|
+
function show(icon, title, msg, color) {
|
|
68
|
+
document.getElementById('spinner').textContent = icon;
|
|
69
|
+
document.getElementById('spinner').style.color = color;
|
|
70
|
+
document.getElementById('title').textContent = title;
|
|
71
|
+
document.getElementById('msg').textContent = msg;
|
|
72
|
+
}
|
|
73
|
+
if (error) { show('\\u2717', 'Authentication Failed', error, '#ef4444'); return; }
|
|
74
|
+
if (!code) { show('\\u2717', 'Authentication Failed', 'No authorization code received.', '#ef4444'); return; }
|
|
75
|
+
if (!relayUrl) { show('\\u2713', 'Authentication Successful', 'Credentials saved. You can close this tab.', '#10b981'); return; }
|
|
76
|
+
fetch(relayUrl, {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: { 'Content-Type': 'application/json' },
|
|
79
|
+
body: JSON.stringify({ code: code, state: state })
|
|
80
|
+
}).then(function(r) { return r.json(); }).then(function(data) {
|
|
81
|
+
if (data.success) {
|
|
82
|
+
show('\\u2713', 'Authentication Successful', data.email ? 'Signed in as ' + data.email + '. You can close this tab.' : 'Credentials saved. You can close this tab.', '#10b981');
|
|
83
|
+
} else {
|
|
84
|
+
show('\\u2717', 'Authentication Failed', data.error || 'Unknown error', '#ef4444');
|
|
85
|
+
}
|
|
86
|
+
}).catch(function(e) {
|
|
87
|
+
show('\\u2717', 'Relay Failed', 'Could not reach server: ' + e.message + '. You may need to paste the URL manually.', '#ef4444');
|
|
88
|
+
});
|
|
89
|
+
})();
|
|
90
|
+
</script>
|
|
91
|
+
</body></html>`;
|
|
92
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import crypto from 'crypto';
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
import { OAuth2Client } from 'google-auth-library';
|
|
7
|
+
import { findCommand } from './agent-discovery.js';
|
|
8
|
+
import { buildBaseUrl, isRemoteRequest, encodeOAuthState, decodeOAuthState, oauthResultPage, oauthRelayPage } from './oauth-common.js';
|
|
9
|
+
|
|
10
|
+
const GEMINI_SCOPES = [
|
|
11
|
+
'https://www.googleapis.com/auth/cloud-platform',
|
|
12
|
+
'https://www.googleapis.com/auth/userinfo.email',
|
|
13
|
+
'https://www.googleapis.com/auth/userinfo.profile',
|
|
14
|
+
];
|
|
15
|
+
const GEMINI_DIR = path.join(os.homedir(), '.gemini');
|
|
16
|
+
const GEMINI_OAUTH_FILE = path.join(GEMINI_DIR, 'oauth_creds.json');
|
|
17
|
+
const GEMINI_ACCOUNTS_FILE = path.join(GEMINI_DIR, 'google_accounts.json');
|
|
18
|
+
|
|
19
|
+
let geminiOAuthState = { status: 'idle', error: null, email: null };
|
|
20
|
+
let geminiOAuthPending = null;
|
|
21
|
+
|
|
22
|
+
function extractOAuthFromFile(oauth2Path) {
|
|
23
|
+
try {
|
|
24
|
+
const src = fs.readFileSync(oauth2Path, 'utf8');
|
|
25
|
+
const idMatch = src.match(/OAUTH_CLIENT_ID\s*=\s*['"]([^'"]+)['"]/);
|
|
26
|
+
const secretMatch = src.match(/OAUTH_CLIENT_SECRET\s*=\s*['"]([^'"]+)['"]/);
|
|
27
|
+
if (idMatch && secretMatch) return { clientId: idMatch[1], clientSecret: secretMatch[1] };
|
|
28
|
+
} catch {}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getGeminiOAuthCreds(rootDir) {
|
|
33
|
+
if (process.env.GOOGLE_OAUTH_CLIENT_ID && process.env.GOOGLE_OAUTH_CLIENT_SECRET) {
|
|
34
|
+
return { clientId: process.env.GOOGLE_OAUTH_CLIENT_ID, clientSecret: process.env.GOOGLE_OAUTH_CLIENT_SECRET, custom: true };
|
|
35
|
+
}
|
|
36
|
+
const oauthRelPath = path.join('node_modules', '@google', 'gemini-cli-core', 'dist', 'src', 'code_assist', 'oauth2.js');
|
|
37
|
+
try {
|
|
38
|
+
const geminiPath = findCommand('gemini', rootDir);
|
|
39
|
+
if (geminiPath) {
|
|
40
|
+
const realPath = fs.realpathSync(geminiPath);
|
|
41
|
+
const pkgRoot = path.resolve(path.dirname(realPath), '..');
|
|
42
|
+
const result = extractOAuthFromFile(path.join(pkgRoot, oauthRelPath));
|
|
43
|
+
if (result) return result;
|
|
44
|
+
}
|
|
45
|
+
} catch (e) {
|
|
46
|
+
console.error('[gemini-oauth] gemini lookup failed:', e.message);
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const npmCacheDirs = new Set();
|
|
50
|
+
const addDir = (d) => { if (d) npmCacheDirs.add(path.join(d, '_npx')); };
|
|
51
|
+
addDir(path.join(os.homedir(), '.npm'));
|
|
52
|
+
addDir(path.join(os.homedir(), '.cache', '.npm'));
|
|
53
|
+
if (process.env.NPM_CACHE) addDir(process.env.NPM_CACHE);
|
|
54
|
+
if (process.env.npm_config_cache) addDir(process.env.npm_config_cache);
|
|
55
|
+
try { addDir(execSync('npm config get cache', { encoding: 'utf8', timeout: 5000 }).trim()); } catch {}
|
|
56
|
+
for (const cacheDir of npmCacheDirs) {
|
|
57
|
+
if (!fs.existsSync(cacheDir)) continue;
|
|
58
|
+
for (const d of fs.readdirSync(cacheDir).filter(d => !d.startsWith('.'))) {
|
|
59
|
+
const result = extractOAuthFromFile(path.join(cacheDir, d, oauthRelPath));
|
|
60
|
+
if (result) return result;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} catch (e) {
|
|
64
|
+
console.error('[gemini-oauth] npm cache scan failed:', e.message);
|
|
65
|
+
}
|
|
66
|
+
console.error('[gemini-oauth] Could not find Gemini CLI OAuth credentials in any known location');
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function saveGeminiCredentials(tokens, email) {
|
|
71
|
+
if (!fs.existsSync(GEMINI_DIR)) fs.mkdirSync(GEMINI_DIR, { recursive: true });
|
|
72
|
+
fs.writeFileSync(GEMINI_OAUTH_FILE, JSON.stringify(tokens, null, 2), { mode: 0o600 });
|
|
73
|
+
try { fs.chmodSync(GEMINI_OAUTH_FILE, 0o600); } catch (_) {}
|
|
74
|
+
let accounts = { active: null, old: [] };
|
|
75
|
+
try {
|
|
76
|
+
if (fs.existsSync(GEMINI_ACCOUNTS_FILE)) {
|
|
77
|
+
accounts = JSON.parse(fs.readFileSync(GEMINI_ACCOUNTS_FILE, 'utf8'));
|
|
78
|
+
}
|
|
79
|
+
} catch (_) {}
|
|
80
|
+
if (email) {
|
|
81
|
+
if (accounts.active && accounts.active !== email && !accounts.old.includes(accounts.active)) {
|
|
82
|
+
accounts.old.push(accounts.active);
|
|
83
|
+
}
|
|
84
|
+
accounts.active = email;
|
|
85
|
+
}
|
|
86
|
+
fs.writeFileSync(GEMINI_ACCOUNTS_FILE, JSON.stringify(accounts, null, 2), { mode: 0o600 });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function startGeminiOAuth(req, { PORT, BASE_URL, rootDir }) {
|
|
90
|
+
const creds = getGeminiOAuthCreds(rootDir);
|
|
91
|
+
if (!creds) throw new Error('Could not find Gemini CLI OAuth credentials. Install gemini CLI first.');
|
|
92
|
+
const useCustomClient = !!creds.custom;
|
|
93
|
+
const remote = isRemoteRequest(req);
|
|
94
|
+
let redirectUri;
|
|
95
|
+
if (useCustomClient && req) {
|
|
96
|
+
redirectUri = `${buildBaseUrl(req, PORT)}${BASE_URL}/oauth2callback`;
|
|
97
|
+
} else {
|
|
98
|
+
redirectUri = `http://localhost:${PORT}${BASE_URL}/oauth2callback`;
|
|
99
|
+
}
|
|
100
|
+
const csrfToken = crypto.randomBytes(32).toString('hex');
|
|
101
|
+
const relayUrl = req ? `${buildBaseUrl(req, PORT)}${BASE_URL}/api/gemini-oauth/relay` : null;
|
|
102
|
+
const state = encodeOAuthState(csrfToken, relayUrl);
|
|
103
|
+
const client = new OAuth2Client({ clientId: creds.clientId, clientSecret: creds.clientSecret });
|
|
104
|
+
const authUrl = client.generateAuthUrl({ redirect_uri: redirectUri, access_type: 'offline', scope: GEMINI_SCOPES, state });
|
|
105
|
+
const mode = useCustomClient ? 'custom' : (remote ? 'cli-remote' : 'cli-local');
|
|
106
|
+
geminiOAuthPending = { client, redirectUri, state: csrfToken };
|
|
107
|
+
geminiOAuthState = { status: 'pending', error: null, email: null };
|
|
108
|
+
setTimeout(() => {
|
|
109
|
+
if (geminiOAuthState.status === 'pending') {
|
|
110
|
+
geminiOAuthState = { status: 'error', error: 'Authentication timed out', email: null };
|
|
111
|
+
geminiOAuthPending = null;
|
|
112
|
+
}
|
|
113
|
+
}, 5 * 60 * 1000);
|
|
114
|
+
return { authUrl, mode };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function exchangeGeminiOAuthCode(code, stateParam) {
|
|
118
|
+
if (!geminiOAuthPending) throw new Error('No pending OAuth flow. Please start authentication again.');
|
|
119
|
+
const { client, redirectUri, state: expectedCsrf } = geminiOAuthPending;
|
|
120
|
+
const { csrfToken } = decodeOAuthState(stateParam);
|
|
121
|
+
if (csrfToken !== expectedCsrf) {
|
|
122
|
+
geminiOAuthState = { status: 'error', error: 'State mismatch', email: null };
|
|
123
|
+
geminiOAuthPending = null;
|
|
124
|
+
throw new Error('State mismatch - possible CSRF attack.');
|
|
125
|
+
}
|
|
126
|
+
if (!code) {
|
|
127
|
+
geminiOAuthState = { status: 'error', error: 'No authorization code received', email: null };
|
|
128
|
+
geminiOAuthPending = null;
|
|
129
|
+
throw new Error('No authorization code received.');
|
|
130
|
+
}
|
|
131
|
+
const { tokens } = await client.getToken({ code, redirect_uri: redirectUri });
|
|
132
|
+
client.setCredentials(tokens);
|
|
133
|
+
let email = '';
|
|
134
|
+
try {
|
|
135
|
+
const { token } = await client.getAccessToken();
|
|
136
|
+
if (token) {
|
|
137
|
+
const resp = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', { headers: { Authorization: `Bearer ${token}` } });
|
|
138
|
+
if (resp.ok) { const info = await resp.json(); email = info.email || ''; }
|
|
139
|
+
}
|
|
140
|
+
} catch (_) {}
|
|
141
|
+
saveGeminiCredentials(tokens, email);
|
|
142
|
+
geminiOAuthState = { status: 'success', error: null, email };
|
|
143
|
+
geminiOAuthPending = null;
|
|
144
|
+
return email;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function handleGeminiOAuthCallback(req, res, PORT) {
|
|
148
|
+
const reqUrl = new URL(req.url, `http://localhost:${PORT}`);
|
|
149
|
+
const code = reqUrl.searchParams.get('code');
|
|
150
|
+
const state = reqUrl.searchParams.get('state');
|
|
151
|
+
const error = reqUrl.searchParams.get('error');
|
|
152
|
+
const errorDesc = reqUrl.searchParams.get('error_description');
|
|
153
|
+
if (error) {
|
|
154
|
+
const desc = errorDesc || error;
|
|
155
|
+
geminiOAuthState = { status: 'error', error: desc, email: null };
|
|
156
|
+
geminiOAuthPending = null;
|
|
157
|
+
}
|
|
158
|
+
const stateData = decodeOAuthState(state || '');
|
|
159
|
+
if (stateData.relayUrl) {
|
|
160
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
161
|
+
res.end(oauthRelayPage(code, state, errorDesc || error));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (!geminiOAuthPending) {
|
|
165
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
166
|
+
res.end(oauthResultPage('Authentication Failed', 'No pending OAuth flow.', false));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
if (error) throw new Error(errorDesc || error);
|
|
171
|
+
const email = await exchangeGeminiOAuthCode(code, state);
|
|
172
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
173
|
+
res.end(oauthResultPage('Authentication Successful', email ? `Signed in as ${email}` : 'Gemini CLI credentials saved.', true));
|
|
174
|
+
} catch (e) {
|
|
175
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
176
|
+
res.end(oauthResultPage('Authentication Failed', e.message, false));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function getGeminiOAuthStatus() {
|
|
181
|
+
try {
|
|
182
|
+
if (fs.existsSync(GEMINI_OAUTH_FILE)) {
|
|
183
|
+
const creds = JSON.parse(fs.readFileSync(GEMINI_OAUTH_FILE, 'utf8'));
|
|
184
|
+
if (creds.refresh_token || creds.access_token) {
|
|
185
|
+
let email = '';
|
|
186
|
+
try {
|
|
187
|
+
if (fs.existsSync(GEMINI_ACCOUNTS_FILE)) {
|
|
188
|
+
const accts = JSON.parse(fs.readFileSync(GEMINI_ACCOUNTS_FILE, 'utf8'));
|
|
189
|
+
email = accts.active || '';
|
|
190
|
+
}
|
|
191
|
+
} catch (_) {}
|
|
192
|
+
return { hasKey: true, apiKey: email || '****oauth', defaultModel: '', path: GEMINI_OAUTH_FILE, authMethod: 'oauth' };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
} catch (_) {}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function getGeminiOAuthState() { return geminiOAuthState; }
|
|
200
|
+
export function getGeminiOAuthPending() { return geminiOAuthPending; }
|
package/package.json
CHANGED
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,8 @@ 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 { startCodexOAuth, exchangeCodexOAuthCode, handleCodexOAuthCallback, getCodexOAuthStatus, getCodexOAuthState, CODEX_HOME, CODEX_AUTH_FILE } from './lib/oauth-codex.js';
|
|
21
22
|
import { WSOptimizer } from './lib/ws-optimizer.js';
|
|
22
23
|
import { WsRouter } from './lib/ws-protocol.js';
|
|
23
24
|
import { encode as wsEncode } from './lib/codec.js';
|
|
@@ -483,551 +484,6 @@ async function getModelsForAgent(agentId) {
|
|
|
483
484
|
return models;
|
|
484
485
|
}
|
|
485
486
|
|
|
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
487
|
const PROVIDER_CONFIGS = {
|
|
1032
488
|
'anthropic': {
|
|
1033
489
|
name: 'Anthropic', configPaths: [
|
|
@@ -1281,12 +737,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
1281
737
|
const pathOnly = routePath.split('?')[0];
|
|
1282
738
|
|
|
1283
739
|
if (pathOnly === '/oauth2callback' && req.method === 'GET') {
|
|
1284
|
-
await handleGeminiOAuthCallback(req, res);
|
|
740
|
+
await handleGeminiOAuthCallback(req, res, PORT);
|
|
1285
741
|
return;
|
|
1286
742
|
}
|
|
1287
743
|
|
|
1288
744
|
if (pathOnly === '/codex-oauth2callback' && req.method === 'GET') {
|
|
1289
|
-
await handleCodexOAuthCallback(req, res);
|
|
745
|
+
await handleCodexOAuthCallback(req, res, PORT);
|
|
1290
746
|
return;
|
|
1291
747
|
}
|
|
1292
748
|
|
|
@@ -2746,7 +2202,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
2746
2202
|
|
|
2747
2203
|
if (pathOnly === '/api/gemini-oauth/start' && req.method === 'POST') {
|
|
2748
2204
|
try {
|
|
2749
|
-
const result = await startGeminiOAuth(req);
|
|
2205
|
+
const result = await startGeminiOAuth(req, { PORT, BASE_URL, rootDir });
|
|
2750
2206
|
sendJSON(req, res, 200, { authUrl: result.authUrl, mode: result.mode });
|
|
2751
2207
|
} catch (e) {
|
|
2752
2208
|
console.error('[gemini-oauth] /api/gemini-oauth/start failed:', e);
|
|
@@ -2756,7 +2212,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
2756
2212
|
}
|
|
2757
2213
|
|
|
2758
2214
|
if (pathOnly === '/api/gemini-oauth/status' && req.method === 'GET') {
|
|
2759
|
-
sendJSON(req, res, 200,
|
|
2215
|
+
sendJSON(req, res, 200, getGeminiOAuthState());
|
|
2760
2216
|
return;
|
|
2761
2217
|
}
|
|
2762
2218
|
|
|
@@ -2771,8 +2227,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
2771
2227
|
const email = await exchangeGeminiOAuthCode(code, stateParam);
|
|
2772
2228
|
sendJSON(req, res, 200, { success: true, email });
|
|
2773
2229
|
} catch (e) {
|
|
2774
|
-
geminiOAuthState = { status: 'error', error: e.message, email: null };
|
|
2775
|
-
geminiOAuthPending = null;
|
|
2776
2230
|
sendJSON(req, res, 400, { error: e.message });
|
|
2777
2231
|
}
|
|
2778
2232
|
return;
|
|
@@ -2796,8 +2250,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
2796
2250
|
const error = parsed.searchParams.get('error');
|
|
2797
2251
|
if (error) {
|
|
2798
2252
|
const desc = parsed.searchParams.get('error_description') || error;
|
|
2799
|
-
geminiOAuthState = { status: 'error', error: desc, email: null };
|
|
2800
|
-
geminiOAuthPending = null;
|
|
2801
2253
|
sendJSON(req, res, 200, { error: desc });
|
|
2802
2254
|
return;
|
|
2803
2255
|
}
|
|
@@ -2807,8 +2259,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
2807
2259
|
const email = await exchangeGeminiOAuthCode(code, state);
|
|
2808
2260
|
sendJSON(req, res, 200, { success: true, email });
|
|
2809
2261
|
} catch (e) {
|
|
2810
|
-
geminiOAuthState = { status: 'error', error: e.message, email: null };
|
|
2811
|
-
geminiOAuthPending = null;
|
|
2812
2262
|
sendJSON(req, res, 400, { error: e.message });
|
|
2813
2263
|
}
|
|
2814
2264
|
return;
|
|
@@ -2816,7 +2266,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
2816
2266
|
|
|
2817
2267
|
if (pathOnly === '/api/codex-oauth/start' && req.method === 'POST') {
|
|
2818
2268
|
try {
|
|
2819
|
-
const result = await startCodexOAuth(req);
|
|
2269
|
+
const result = await startCodexOAuth(req, { PORT, BASE_URL });
|
|
2820
2270
|
sendJSON(req, res, 200, { authUrl: result.authUrl, mode: result.mode });
|
|
2821
2271
|
} catch (e) {
|
|
2822
2272
|
console.error('[codex-oauth] /api/codex-oauth/start failed:', e);
|
|
@@ -2826,7 +2276,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
2826
2276
|
}
|
|
2827
2277
|
|
|
2828
2278
|
if (pathOnly === '/api/codex-oauth/status' && req.method === 'GET') {
|
|
2829
|
-
sendJSON(req, res, 200,
|
|
2279
|
+
sendJSON(req, res, 200, getCodexOAuthState());
|
|
2830
2280
|
return;
|
|
2831
2281
|
}
|
|
2832
2282
|
|
|
@@ -2841,8 +2291,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
2841
2291
|
const email = await exchangeCodexOAuthCode(code, stateParam);
|
|
2842
2292
|
sendJSON(req, res, 200, { success: true, email });
|
|
2843
2293
|
} catch (e) {
|
|
2844
|
-
codexOAuthState = { status: 'error', error: e.message, email: null };
|
|
2845
|
-
codexOAuthPending = null;
|
|
2846
2294
|
sendJSON(req, res, 400, { error: e.message });
|
|
2847
2295
|
}
|
|
2848
2296
|
return;
|
|
@@ -2864,8 +2312,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
2864
2312
|
const error = parsed.searchParams.get('error');
|
|
2865
2313
|
if (error) {
|
|
2866
2314
|
const desc = parsed.searchParams.get('error_description') || error;
|
|
2867
|
-
codexOAuthState = { status: 'error', error: desc, email: null };
|
|
2868
|
-
codexOAuthPending = null;
|
|
2869
2315
|
sendJSON(req, res, 200, { error: desc });
|
|
2870
2316
|
return;
|
|
2871
2317
|
}
|
|
@@ -2874,8 +2320,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
2874
2320
|
const email = await exchangeCodexOAuthCode(code, state);
|
|
2875
2321
|
sendJSON(req, res, 200, { success: true, email });
|
|
2876
2322
|
} catch (e) {
|
|
2877
|
-
codexOAuthState = { status: 'error', error: e.message, email: null };
|
|
2878
|
-
codexOAuthPending = null;
|
|
2879
2323
|
sendJSON(req, res, 400, { error: e.message });
|
|
2880
2324
|
}
|
|
2881
2325
|
return;
|
|
@@ -2889,21 +2333,21 @@ const server = http.createServer(async (req, res) => {
|
|
|
2889
2333
|
|
|
2890
2334
|
if (agentId === 'codex' || agentId === 'cli-codex') {
|
|
2891
2335
|
try {
|
|
2892
|
-
const result = await startCodexOAuth(req);
|
|
2336
|
+
const result = await startCodexOAuth(req, { PORT, BASE_URL });
|
|
2893
2337
|
const conversationId = '__agent_auth__';
|
|
2894
2338
|
broadcastSync({ type: 'script_started', conversationId, script: 'auth-codex', agentId: 'codex', timestamp: Date.now() });
|
|
2895
2339
|
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
2340
|
|
|
2897
2341
|
const pollId = setInterval(() => {
|
|
2898
|
-
if (
|
|
2342
|
+
if (getCodexOAuthState().status === 'success') {
|
|
2899
2343
|
clearInterval(pollId);
|
|
2900
|
-
const email =
|
|
2344
|
+
const email = getCodexOAuthState().email || '';
|
|
2901
2345
|
broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[32mAuthentication successful${email ? ' (' + email + ')' : ''}\x1b[0m\r\n`, stream: 'stdout', timestamp: Date.now() });
|
|
2902
2346
|
broadcastSync({ type: 'script_stopped', conversationId, code: 0, timestamp: Date.now() });
|
|
2903
|
-
} else if (
|
|
2347
|
+
} else if (getCodexOAuthState().status === 'error') {
|
|
2904
2348
|
clearInterval(pollId);
|
|
2905
|
-
broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[31mAuthentication failed: ${
|
|
2906
|
-
broadcastSync({ type: 'script_stopped', conversationId, code: 1, error:
|
|
2349
|
+
broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[31mAuthentication failed: ${getCodexOAuthState().error}\x1b[0m\r\n`, stream: 'stderr', timestamp: Date.now() });
|
|
2350
|
+
broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: getCodexOAuthState().error, timestamp: Date.now() });
|
|
2907
2351
|
}
|
|
2908
2352
|
}, 1000);
|
|
2909
2353
|
|
|
@@ -2920,21 +2364,21 @@ const server = http.createServer(async (req, res) => {
|
|
|
2920
2364
|
|
|
2921
2365
|
if (agentId === 'gemini') {
|
|
2922
2366
|
try {
|
|
2923
|
-
const result = await startGeminiOAuth(req);
|
|
2367
|
+
const result = await startGeminiOAuth(req, { PORT, BASE_URL, rootDir });
|
|
2924
2368
|
const conversationId = '__agent_auth__';
|
|
2925
2369
|
broadcastSync({ type: 'script_started', conversationId, script: 'auth-gemini', agentId: 'gemini', timestamp: Date.now() });
|
|
2926
2370
|
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
2371
|
|
|
2928
2372
|
const pollId = setInterval(() => {
|
|
2929
|
-
if (
|
|
2373
|
+
if (getGeminiOAuthState().status === 'success') {
|
|
2930
2374
|
clearInterval(pollId);
|
|
2931
|
-
const email =
|
|
2375
|
+
const email = getGeminiOAuthState().email || '';
|
|
2932
2376
|
broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[32mAuthentication successful${email ? ' (' + email + ')' : ''}\x1b[0m\r\n`, stream: 'stdout', timestamp: Date.now() });
|
|
2933
2377
|
broadcastSync({ type: 'script_stopped', conversationId, code: 0, timestamp: Date.now() });
|
|
2934
|
-
} else if (
|
|
2378
|
+
} else if (getGeminiOAuthState().status === 'error') {
|
|
2935
2379
|
clearInterval(pollId);
|
|
2936
|
-
broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[31mAuthentication failed: ${
|
|
2937
|
-
broadcastSync({ type: 'script_stopped', conversationId, code: 1, error:
|
|
2380
|
+
broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[31mAuthentication failed: ${getGeminiOAuthState().error}\x1b[0m\r\n`, stream: 'stderr', timestamp: Date.now() });
|
|
2381
|
+
broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: getGeminiOAuthState().error, timestamp: Date.now() });
|
|
2938
2382
|
}
|
|
2939
2383
|
}, 1000);
|
|
2940
2384
|
|
|
@@ -4516,7 +3960,7 @@ console.log('[INIT] About to call registerSessionHandlers, discoveredAgents.leng
|
|
|
4516
3960
|
registerSessionHandlers(wsRouter, {
|
|
4517
3961
|
db: queries, discoveredAgents, modelCache,
|
|
4518
3962
|
getAgentDescriptor, activeScripts, broadcastSync,
|
|
4519
|
-
startGeminiOAuth
|
|
3963
|
+
startGeminiOAuth: (req) => startGeminiOAuth(req, { PORT, BASE_URL, rootDir }), geminiOAuthState: getGeminiOAuthState
|
|
4520
3964
|
});
|
|
4521
3965
|
console.log('[INIT] registerSessionHandlers completed');
|
|
4522
3966
|
|
|
@@ -4536,10 +3980,12 @@ registerScriptHandlers(wsRouter, {
|
|
|
4536
3980
|
});
|
|
4537
3981
|
|
|
4538
3982
|
registerOAuthHandlers(wsRouter, {
|
|
4539
|
-
startGeminiOAuth,
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
3983
|
+
startGeminiOAuth: (req) => startGeminiOAuth(req, { PORT, BASE_URL, rootDir }),
|
|
3984
|
+
exchangeGeminiOAuthCode,
|
|
3985
|
+
geminiOAuthState: getGeminiOAuthState,
|
|
3986
|
+
startCodexOAuth: (req) => startCodexOAuth(req, { PORT, BASE_URL }),
|
|
3987
|
+
exchangeCodexOAuthCode,
|
|
3988
|
+
codexOAuthState: getCodexOAuthState,
|
|
4543
3989
|
});
|
|
4544
3990
|
|
|
4545
3991
|
wsRouter.onLegacy((data, ws) => {
|