agentgui 1.0.209 → 1.0.210
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/package.json +1 -1
- package/server.js +89 -50
- package/static/js/agent-auth.js +99 -0
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -333,11 +333,11 @@ function geminiOAuthResultPage(title, message, success) {
|
|
|
333
333
|
</div></body></html>`;
|
|
334
334
|
}
|
|
335
335
|
|
|
336
|
-
async function startGeminiOAuth(
|
|
336
|
+
async function startGeminiOAuth() {
|
|
337
337
|
const creds = getGeminiOAuthCreds();
|
|
338
338
|
if (!creds) throw new Error('Could not find Gemini CLI OAuth credentials. Install gemini CLI first.');
|
|
339
339
|
|
|
340
|
-
const redirectUri =
|
|
340
|
+
const redirectUri = `http://localhost:${PORT}${BASE_URL}/oauth2callback`;
|
|
341
341
|
const state = crypto.randomBytes(32).toString('hex');
|
|
342
342
|
|
|
343
343
|
const client = new OAuth2Client({
|
|
@@ -365,71 +365,74 @@ async function startGeminiOAuth(baseUrl) {
|
|
|
365
365
|
return authUrl;
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
-
async function
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
if (!geminiOAuthPending) {
|
|
372
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
373
|
-
res.end(geminiOAuthResultPage('Authentication Failed', 'No pending OAuth flow.', false));
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
const error = reqUrl.searchParams.get('error');
|
|
378
|
-
if (error) {
|
|
379
|
-
const desc = reqUrl.searchParams.get('error_description') || error;
|
|
380
|
-
geminiOAuthState = { status: 'error', error: desc, email: null };
|
|
381
|
-
geminiOAuthPending = null;
|
|
382
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
383
|
-
res.end(geminiOAuthResultPage('Authentication Failed', desc, false));
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
368
|
+
async function exchangeGeminiOAuthCode(code, state) {
|
|
369
|
+
if (!geminiOAuthPending) throw new Error('No pending OAuth flow. Please start authentication again.');
|
|
386
370
|
|
|
387
371
|
const { client, redirectUri, state: expectedState } = geminiOAuthPending;
|
|
388
372
|
|
|
389
|
-
if (
|
|
373
|
+
if (state !== expectedState) {
|
|
390
374
|
geminiOAuthState = { status: 'error', error: 'State mismatch', email: null };
|
|
391
375
|
geminiOAuthPending = null;
|
|
392
|
-
|
|
393
|
-
res.end(geminiOAuthResultPage('Authentication Failed', 'State mismatch.', false));
|
|
394
|
-
return;
|
|
376
|
+
throw new Error('State mismatch - possible CSRF attack.');
|
|
395
377
|
}
|
|
396
378
|
|
|
397
|
-
const code = reqUrl.searchParams.get('code');
|
|
398
379
|
if (!code) {
|
|
399
|
-
geminiOAuthState = { status: 'error', error: 'No authorization code', email: null };
|
|
380
|
+
geminiOAuthState = { status: 'error', error: 'No authorization code received', email: null };
|
|
400
381
|
geminiOAuthPending = null;
|
|
382
|
+
throw new Error('No authorization code received.');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const { tokens } = await client.getToken({ code, redirect_uri: redirectUri });
|
|
386
|
+
client.setCredentials(tokens);
|
|
387
|
+
|
|
388
|
+
let email = '';
|
|
389
|
+
try {
|
|
390
|
+
const { token } = await client.getAccessToken();
|
|
391
|
+
if (token) {
|
|
392
|
+
const resp = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
|
|
393
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
394
|
+
});
|
|
395
|
+
if (resp.ok) {
|
|
396
|
+
const info = await resp.json();
|
|
397
|
+
email = info.email || '';
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
} catch (_) {}
|
|
401
|
+
|
|
402
|
+
saveGeminiCredentials(tokens, email);
|
|
403
|
+
geminiOAuthState = { status: 'success', error: null, email };
|
|
404
|
+
geminiOAuthPending = null;
|
|
405
|
+
|
|
406
|
+
return email;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async function handleGeminiOAuthCallback(req, res) {
|
|
410
|
+
const reqUrl = new URL(req.url, `http://localhost:${PORT}`);
|
|
411
|
+
|
|
412
|
+
if (!geminiOAuthPending) {
|
|
401
413
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
402
|
-
res.end(geminiOAuthResultPage('Authentication Failed', 'No
|
|
414
|
+
res.end(geminiOAuthResultPage('Authentication Failed', 'No pending OAuth flow.', false));
|
|
403
415
|
return;
|
|
404
416
|
}
|
|
405
417
|
|
|
406
418
|
try {
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
});
|
|
417
|
-
if (resp.ok) {
|
|
418
|
-
const info = await resp.json();
|
|
419
|
-
email = info.email || '';
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
} catch (_) {}
|
|
419
|
+
const error = reqUrl.searchParams.get('error');
|
|
420
|
+
if (error) {
|
|
421
|
+
const desc = reqUrl.searchParams.get('error_description') || error;
|
|
422
|
+
geminiOAuthState = { status: 'error', error: desc, email: null };
|
|
423
|
+
geminiOAuthPending = null;
|
|
424
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
425
|
+
res.end(geminiOAuthResultPage('Authentication Failed', desc, false));
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
423
428
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
429
|
+
const code = reqUrl.searchParams.get('code');
|
|
430
|
+
const state = reqUrl.searchParams.get('state');
|
|
431
|
+
const email = await exchangeGeminiOAuthCode(code, state);
|
|
427
432
|
|
|
428
433
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
429
434
|
res.end(geminiOAuthResultPage('Authentication Successful', email ? `Signed in as ${email}` : 'Gemini CLI credentials saved.', true));
|
|
430
435
|
} catch (e) {
|
|
431
|
-
geminiOAuthState = { status: 'error', error: e.message, email: null };
|
|
432
|
-
geminiOAuthPending = null;
|
|
433
436
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
434
437
|
res.end(geminiOAuthResultPage('Authentication Failed', e.message, false));
|
|
435
438
|
}
|
|
@@ -1127,7 +1130,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1127
1130
|
|
|
1128
1131
|
if (pathOnly === '/api/gemini-oauth/start' && req.method === 'POST') {
|
|
1129
1132
|
try {
|
|
1130
|
-
const authUrl = await startGeminiOAuth(
|
|
1133
|
+
const authUrl = await startGeminiOAuth();
|
|
1131
1134
|
sendJSON(req, res, 200, { authUrl });
|
|
1132
1135
|
} catch (e) {
|
|
1133
1136
|
console.error('[gemini-oauth] /api/gemini-oauth/start failed:', e);
|
|
@@ -1141,6 +1144,42 @@ const server = http.createServer(async (req, res) => {
|
|
|
1141
1144
|
return;
|
|
1142
1145
|
}
|
|
1143
1146
|
|
|
1147
|
+
if (pathOnly === '/api/gemini-oauth/complete' && req.method === 'POST') {
|
|
1148
|
+
try {
|
|
1149
|
+
const body = await parseBody(req);
|
|
1150
|
+
const pastedUrl = (body.url || '').trim();
|
|
1151
|
+
if (!pastedUrl) {
|
|
1152
|
+
sendJSON(req, res, 400, { error: 'No URL provided' });
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
let parsed;
|
|
1157
|
+
try { parsed = new URL(pastedUrl); } catch (_) {
|
|
1158
|
+
sendJSON(req, res, 400, { error: 'Invalid URL. Paste the full URL from the browser address bar.' });
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
const error = parsed.searchParams.get('error');
|
|
1163
|
+
if (error) {
|
|
1164
|
+
const desc = parsed.searchParams.get('error_description') || error;
|
|
1165
|
+
geminiOAuthState = { status: 'error', error: desc, email: null };
|
|
1166
|
+
geminiOAuthPending = null;
|
|
1167
|
+
sendJSON(req, res, 200, { error: desc });
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
const code = parsed.searchParams.get('code');
|
|
1172
|
+
const state = parsed.searchParams.get('state');
|
|
1173
|
+
const email = await exchangeGeminiOAuthCode(code, state);
|
|
1174
|
+
sendJSON(req, res, 200, { success: true, email });
|
|
1175
|
+
} catch (e) {
|
|
1176
|
+
geminiOAuthState = { status: 'error', error: e.message, email: null };
|
|
1177
|
+
geminiOAuthPending = null;
|
|
1178
|
+
sendJSON(req, res, 400, { error: e.message });
|
|
1179
|
+
}
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1144
1183
|
const agentAuthMatch = pathOnly.match(/^\/api\/agents\/([^/]+)\/auth$/);
|
|
1145
1184
|
if (agentAuthMatch && req.method === 'POST') {
|
|
1146
1185
|
const agentId = agentAuthMatch[1];
|
|
@@ -1149,7 +1188,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1149
1188
|
|
|
1150
1189
|
if (agentId === 'gemini') {
|
|
1151
1190
|
try {
|
|
1152
|
-
const authUrl = await startGeminiOAuth(
|
|
1191
|
+
const authUrl = await startGeminiOAuth();
|
|
1153
1192
|
const conversationId = '__agent_auth__';
|
|
1154
1193
|
broadcastSync({ type: 'script_started', conversationId, script: 'auth-gemini', agentId: 'gemini', timestamp: Date.now() });
|
|
1155
1194
|
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${authUrl}\r\n`, stream: 'stdout', timestamp: Date.now() });
|
package/static/js/agent-auth.js
CHANGED
|
@@ -130,6 +130,81 @@
|
|
|
130
130
|
|
|
131
131
|
function closeDropdown() { dropdown.classList.remove('open'); editingProvider = null; }
|
|
132
132
|
|
|
133
|
+
var oauthPollInterval = null, oauthPollTimeout = null;
|
|
134
|
+
|
|
135
|
+
function cleanupOAuthPolling() {
|
|
136
|
+
if (oauthPollInterval) { clearInterval(oauthPollInterval); oauthPollInterval = null; }
|
|
137
|
+
if (oauthPollTimeout) { clearTimeout(oauthPollTimeout); oauthPollTimeout = null; }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function showOAuthPasteModal() {
|
|
141
|
+
removeOAuthPasteModal();
|
|
142
|
+
var overlay = document.createElement('div');
|
|
143
|
+
overlay.id = 'oauthPasteModal';
|
|
144
|
+
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);display:flex;align-items:center;justify-content:center;z-index:9999;';
|
|
145
|
+
var s = function(c) { return 'font-size:0.8rem;color:var(--color-text-secondary,#d1d5db);margin:0 0 ' + (c ? '0' : '0.5rem') + ';'; };
|
|
146
|
+
overlay.innerHTML = '<div style="background:var(--color-bg-secondary,#1f2937);border-radius:1rem;padding:2rem;max-width:28rem;width:calc(100% - 2rem);box-shadow:0 25px 50px rgba(0,0,0,0.5);color:var(--color-text-primary,white);font-family:system-ui,sans-serif;" onclick="event.stopPropagation()">' +
|
|
147
|
+
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">' +
|
|
148
|
+
'<h2 style="font-size:1.125rem;font-weight:700;margin:0;">Complete Google Sign-In</h2>' +
|
|
149
|
+
'<button id="oauthPasteClose" style="background:none;border:none;color:var(--color-text-secondary,#9ca3af);font-size:1.5rem;cursor:pointer;padding:0;line-height:1;">\u00d7</button></div>' +
|
|
150
|
+
'<div style="margin-bottom:1rem;padding:1rem;background:var(--color-bg-tertiary,rgba(255,255,255,0.05));border-radius:0.5rem;">' +
|
|
151
|
+
'<p style="' + s() + '">1. A Google sign-in page has opened in a new tab.</p>' +
|
|
152
|
+
'<p style="' + s() + '">2. Complete the sign-in process with Google.</p>' +
|
|
153
|
+
'<p style="' + s() + '">3. After signing in, you will be redirected to a page that <span style="color:#facc15;font-weight:600;">may not load</span> (this is expected).</p>' +
|
|
154
|
+
'<p style="' + s(1) + '">4. Copy the <span style="color:white;font-weight:600;">entire URL</span> from the address bar and paste it below.</p></div>' +
|
|
155
|
+
'<label style="display:block;font-size:0.8rem;color:var(--color-text-secondary,#d1d5db);margin-bottom:0.5rem;">Paste the redirect URL here:</label>' +
|
|
156
|
+
'<input type="text" id="oauthPasteInput" placeholder="http://localhost:3000/gm/oauth2callback?code=..." style="width:100%;box-sizing:border-box;padding:0.75rem 1rem;background:var(--color-bg-primary,#374151);border:1px solid var(--color-border,#4b5563);border-radius:0.5rem;color:var(--color-text-primary,white);font-size:0.8rem;font-family:monospace;outline:none;" />' +
|
|
157
|
+
'<p id="oauthPasteError" style="font-size:0.75rem;color:#ef4444;margin:0.5rem 0 0;display:none;"></p>' +
|
|
158
|
+
'<div style="display:flex;gap:0.75rem;margin-top:1.25rem;">' +
|
|
159
|
+
'<button id="oauthPasteCancel" style="flex:1;padding:0.625rem;border-radius:0.5rem;border:1px solid var(--color-border,#4b5563);background:transparent;color:var(--color-text-primary,white);font-size:0.8rem;cursor:pointer;font-weight:600;">Cancel</button>' +
|
|
160
|
+
'<button id="oauthPasteSubmit" style="flex:1;padding:0.625rem;border-radius:0.5rem;border:none;background:var(--color-primary,#3b82f6);color:white;font-size:0.8rem;cursor:pointer;font-weight:600;">Complete Sign-In</button></div>' +
|
|
161
|
+
'<p style="font-size:0.7rem;color:var(--color-text-secondary,#6b7280);margin-top:1rem;text-align:center;">If the redirect page loaded successfully, this dialog will close automatically.</p></div>';
|
|
162
|
+
document.body.appendChild(overlay);
|
|
163
|
+
var dismiss = function() { cleanupOAuthPolling(); authRunning = false; removeOAuthPasteModal(); };
|
|
164
|
+
document.getElementById('oauthPasteClose').addEventListener('click', dismiss);
|
|
165
|
+
document.getElementById('oauthPasteCancel').addEventListener('click', dismiss);
|
|
166
|
+
document.getElementById('oauthPasteSubmit').addEventListener('click', submitOAuthPasteUrl);
|
|
167
|
+
document.getElementById('oauthPasteInput').addEventListener('keydown', function(e) { if (e.key === 'Enter') submitOAuthPasteUrl(); });
|
|
168
|
+
setTimeout(function() { var i = document.getElementById('oauthPasteInput'); if (i) i.focus(); }, 100);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function removeOAuthPasteModal() {
|
|
172
|
+
var el = document.getElementById('oauthPasteModal');
|
|
173
|
+
if (el) el.remove();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function submitOAuthPasteUrl() {
|
|
177
|
+
var input = document.getElementById('oauthPasteInput');
|
|
178
|
+
var errorEl = document.getElementById('oauthPasteError');
|
|
179
|
+
var submitBtn = document.getElementById('oauthPasteSubmit');
|
|
180
|
+
if (!input) return;
|
|
181
|
+
var url = input.value.trim();
|
|
182
|
+
if (!url) {
|
|
183
|
+
if (errorEl) { errorEl.textContent = 'Please paste the URL from the redirected page.'; errorEl.style.display = 'block'; }
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = 'Verifying...'; }
|
|
187
|
+
if (errorEl) errorEl.style.display = 'none';
|
|
188
|
+
|
|
189
|
+
fetch(BASE + '/api/gemini-oauth/complete', {
|
|
190
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
191
|
+
body: JSON.stringify({ url: url })
|
|
192
|
+
}).then(function(r) { return r.json(); }).then(function(data) {
|
|
193
|
+
if (data.success) {
|
|
194
|
+
cleanupOAuthPolling();
|
|
195
|
+
authRunning = false;
|
|
196
|
+
removeOAuthPasteModal();
|
|
197
|
+
refresh();
|
|
198
|
+
} else {
|
|
199
|
+
if (errorEl) { errorEl.textContent = data.error || 'Failed to complete authentication.'; errorEl.style.display = 'block'; }
|
|
200
|
+
if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Complete Sign-In'; }
|
|
201
|
+
}
|
|
202
|
+
}).catch(function(e) {
|
|
203
|
+
if (errorEl) { errorEl.textContent = e.message; errorEl.style.display = 'block'; }
|
|
204
|
+
if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Complete Sign-In'; }
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
133
208
|
function triggerAuth(agentId) {
|
|
134
209
|
if (authRunning) return;
|
|
135
210
|
fetch(BASE + '/api/agents/' + agentId + '/auth', {
|
|
@@ -141,6 +216,28 @@
|
|
|
141
216
|
if (term) { term.clear(); term.writeln('\x1b[36m[authenticating ' + agentId + ']\x1b[0m\r\n'); }
|
|
142
217
|
if (data.authUrl) {
|
|
143
218
|
window.open(data.authUrl, '_blank');
|
|
219
|
+
if (agentId === 'gemini') {
|
|
220
|
+
showOAuthPasteModal();
|
|
221
|
+
cleanupOAuthPolling();
|
|
222
|
+
oauthPollInterval = setInterval(function() {
|
|
223
|
+
fetch(BASE + '/api/gemini-oauth/status').then(function(r) { return r.json(); }).then(function(status) {
|
|
224
|
+
if (status.status === 'success') {
|
|
225
|
+
cleanupOAuthPolling();
|
|
226
|
+
authRunning = false;
|
|
227
|
+
removeOAuthPasteModal();
|
|
228
|
+
refresh();
|
|
229
|
+
} else if (status.status === 'error') {
|
|
230
|
+
cleanupOAuthPolling();
|
|
231
|
+
authRunning = false;
|
|
232
|
+
removeOAuthPasteModal();
|
|
233
|
+
}
|
|
234
|
+
}).catch(function() {});
|
|
235
|
+
}, 1500);
|
|
236
|
+
oauthPollTimeout = setTimeout(function() {
|
|
237
|
+
cleanupOAuthPolling();
|
|
238
|
+
if (authRunning) { authRunning = false; removeOAuthPasteModal(); }
|
|
239
|
+
}, 5 * 60 * 1000);
|
|
240
|
+
}
|
|
144
241
|
}
|
|
145
242
|
}
|
|
146
243
|
}).catch(function() {});
|
|
@@ -159,6 +256,8 @@
|
|
|
159
256
|
if (term) term.write(data.data);
|
|
160
257
|
} else if (data.type === 'script_stopped') {
|
|
161
258
|
authRunning = false;
|
|
259
|
+
removeOAuthPasteModal();
|
|
260
|
+
cleanupOAuthPolling();
|
|
162
261
|
var term = getTerminal();
|
|
163
262
|
var msg = data.error ? data.error : ('exited with code ' + (data.code || 0));
|
|
164
263
|
if (term) term.writeln('\r\n\x1b[90m[auth ' + msg + ']\x1b[0m');
|