|
1 | | -import { app, BrowserWindow, session, Menu } from "electron" |
2 | | -import { join } from "path" |
3 | | -import { createServer } from "http" |
4 | | -import { readFileSync, existsSync, unlinkSync, readlinkSync } from "fs" |
5 | 1 | import * as Sentry from "@sentry/electron/main" |
6 | | -import { initDatabase, closeDatabase } from "./lib/db" |
7 | | -import { createMainWindow, getWindow, showLoginPage } from "./windows/main" |
| 2 | +import { app, BrowserWindow, Menu, session } from "electron" |
| 3 | +import { existsSync, readFileSync, readlinkSync, unlinkSync } from "fs" |
| 4 | +import { createServer } from "http" |
| 5 | +import { join } from "path" |
8 | 6 | import { AuthManager } from "./auth-manager" |
9 | 7 | import { |
10 | | - initAnalytics, |
11 | 8 | identify, |
| 9 | + initAnalytics, |
| 10 | + shutdown as shutdownAnalytics, |
12 | 11 | trackAppOpened, |
13 | 12 | trackAuthCompleted, |
14 | | - shutdown as shutdownAnalytics, |
15 | 13 | } from "./lib/analytics" |
16 | 14 | import { |
17 | | - initAutoUpdater, |
18 | 15 | checkForUpdates, |
19 | 16 | downloadUpdate, |
| 17 | + initAutoUpdater, |
20 | 18 | setupFocusUpdateCheck, |
21 | 19 | } from "./lib/auto-updater" |
| 20 | +import { closeDatabase, initDatabase } from "./lib/db" |
22 | 21 | import { cleanupGitWatchers } from "./lib/git/watcher" |
| 22 | +import { cancelAllPendingOAuth, handleMcpOAuthCallback } from "./lib/mcp-auth" |
| 23 | +import { createMainWindow, getWindow } from "./windows/main" |
23 | 24 |
|
24 | 25 | // Dev mode detection |
25 | 26 | const IS_DEV = !!process.env.ELECTRON_RENDERER_URL |
@@ -134,14 +135,24 @@ function handleDeepLink(url: string): void { |
134 | 135 | try { |
135 | 136 | const parsed = new URL(url) |
136 | 137 |
|
137 | | - // Handle auth callback: twentyfirstdev://auth?code=xxx |
| 138 | + // Handle auth callback: twentyfirst-agents://auth?code=xxx |
138 | 139 | if (parsed.pathname === "/auth" || parsed.host === "auth") { |
139 | 140 | const code = parsed.searchParams.get("code") |
140 | 141 | if (code) { |
141 | 142 | handleAuthCode(code) |
142 | 143 | return |
143 | 144 | } |
144 | 145 | } |
| 146 | + |
| 147 | + // Handle MCP OAuth callback: twentyfirst-agents://mcp-oauth?code=xxx&state=yyy |
| 148 | + if (parsed.pathname === "/mcp-oauth" || parsed.host === "mcp-oauth") { |
| 149 | + const code = parsed.searchParams.get("code") |
| 150 | + const state = parsed.searchParams.get("state") |
| 151 | + if (code && state) { |
| 152 | + handleMcpOAuthCallback(code, state) |
| 153 | + return |
| 154 | + } |
| 155 | + } |
145 | 156 | } catch (e) { |
146 | 157 | console.error("[DeepLink] Failed to parse:", e) |
147 | 158 | } |
@@ -219,10 +230,9 @@ console.log("[Protocol] =============================================") |
219 | 230 | const FAVICON_SVG = `<svg width="32" height="32" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="1024" height="1024" fill="#0033FF"/><path fill-rule="evenodd" clip-rule="evenodd" d="M800.165 148C842.048 148 876 181.952 876 223.835V686.415C876 690.606 872.606 694 868.415 694H640.915C636.729 694 633.335 697.394 633.335 701.585V868.415C633.335 872.606 629.936 876 625.75 876H223.835C181.952 876 148 842.048 148 800.165V702.59C148 697.262 150.807 692.326 155.376 689.586L427.843 526.1C434.031 522.388 431.956 513.238 425.327 512.118L423.962 512H155.585C151.394 512 148 508.606 148 504.415V337.585C148 333.394 151.394 330 155.585 330H443.75C447.936 330 451.335 326.606 451.335 322.415V155.585C451.335 151.394 454.729 148 458.915 148H800.165ZM458.915 330C454.729 330 451.335 333.394 451.335 337.585V686.415C451.335 690.606 454.729 694 458.915 694H625.75C629.936 694 633.335 690.606 633.335 686.415V337.585C633.335 333.394 629.936 330 625.75 330H458.915Z" fill="#F4F4F4"/></svg>` |
220 | 231 | const FAVICON_DATA_URI = `data:image/svg+xml,${encodeURIComponent(FAVICON_SVG)}` |
221 | 232 |
|
222 | | -// Dev mode: Start local HTTP server for auth callback |
223 | | -// This catches http://localhost:21321/auth/callback?code=xxx |
224 | | -if (process.env.ELECTRON_RENDERER_URL) { |
225 | | - const server = createServer((req, res) => { |
| 233 | +// Start local HTTP server for auth callbacks |
| 234 | +// This catches http://localhost:21321/auth/callback?code=xxx and /mcp-oauth/callback |
| 235 | +const server = createServer((req, res) => { |
226 | 236 | const url = new URL(req.url || "", "http://localhost:21321") |
227 | 237 |
|
228 | 238 | // Serve favicon |
@@ -312,16 +322,99 @@ if (process.env.ELECTRON_RENDERER_URL) { |
312 | 322 | res.writeHead(400, { "Content-Type": "text/plain" }) |
313 | 323 | res.end("Missing code parameter") |
314 | 324 | } |
| 325 | + } else if (url.pathname === "/mcp-oauth/callback") { |
| 326 | + // Handle MCP OAuth callback in dev mode |
| 327 | + const code = url.searchParams.get("code") |
| 328 | + const state = url.searchParams.get("state") |
| 329 | + console.log( |
| 330 | + "[Auth Server] Received MCP OAuth callback with code:", |
| 331 | + code?.slice(0, 8) + "...", |
| 332 | + "state:", |
| 333 | + state?.slice(0, 8) + "...", |
| 334 | + ) |
| 335 | + |
| 336 | + if (code && state) { |
| 337 | + // Handle the MCP OAuth callback |
| 338 | + handleMcpOAuthCallback(code, state) |
| 339 | + |
| 340 | + // Send success response and close the browser tab |
| 341 | + res.writeHead(200, { "Content-Type": "text/html" }) |
| 342 | + res.end(`<!DOCTYPE html> |
| 343 | +<html> |
| 344 | +<head> |
| 345 | + <meta charset="UTF-8"> |
| 346 | + <link rel="icon" type="image/svg+xml" href="${FAVICON_DATA_URI}"> |
| 347 | + <title>1Code - MCP Authentication</title> |
| 348 | + <style> |
| 349 | + * { margin: 0; padding: 0; box-sizing: border-box; } |
| 350 | + :root { |
| 351 | + --bg: #09090b; |
| 352 | + --text: #fafafa; |
| 353 | + --text-muted: #71717a; |
| 354 | + } |
| 355 | + @media (prefers-color-scheme: light) { |
| 356 | + :root { |
| 357 | + --bg: #ffffff; |
| 358 | + --text: #09090b; |
| 359 | + --text-muted: #71717a; |
| 360 | + } |
| 361 | + } |
| 362 | + body { |
| 363 | + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
| 364 | + display: flex; |
| 365 | + flex-direction: column; |
| 366 | + align-items: center; |
| 367 | + justify-content: center; |
| 368 | + min-height: 100vh; |
| 369 | + background: var(--bg); |
| 370 | + color: var(--text); |
| 371 | + } |
| 372 | + .container { |
| 373 | + display: flex; |
| 374 | + flex-direction: column; |
| 375 | + align-items: center; |
| 376 | + gap: 8px; |
| 377 | + } |
| 378 | + .logo { |
| 379 | + width: 24px; |
| 380 | + height: 24px; |
| 381 | + margin-bottom: 8px; |
| 382 | + } |
| 383 | + h1 { |
| 384 | + font-size: 14px; |
| 385 | + font-weight: 500; |
| 386 | + margin-bottom: 4px; |
| 387 | + } |
| 388 | + p { |
| 389 | + font-size: 12px; |
| 390 | + color: var(--text-muted); |
| 391 | + } |
| 392 | + </style> |
| 393 | +</head> |
| 394 | +<body> |
| 395 | + <div class="container"> |
| 396 | + <svg class="logo" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> |
| 397 | + <path fill-rule="evenodd" clip-rule="evenodd" d="M14.3333 0C15.2538 0 16 0.746192 16 1.66667V11.8333C16 11.9254 15.9254 12 15.8333 12H10.8333C10.7413 12 10.6667 12.0746 10.6667 12.1667V15.8333C10.6667 15.9254 10.592 16 10.5 16H1.66667C0.746192 16 0 15.2538 0 14.3333V12.1888C0 12.0717 0.0617409 11.9632 0.162081 11.903L6.15043 8.30986C6.28644 8.22833 6.24077 8.02716 6.09507 8.00256L6.06511 8H0.166667C0.0746186 8 0 7.92538 0 7.83333V4.16667C0 4.07462 0.0746193 4 0.166667 4H6.5C6.59205 4 6.66667 3.92538 6.66667 3.83333V0.166667C6.66667 0.0746193 6.74129 0 6.83333 0H14.3333ZM6.83333 4C6.74129 4 6.66667 4.07462 6.66667 4.16667V11.8333C6.66667 11.9254 6.74129 12 6.83333 12H10.5C10.592 12 10.6667 11.9254 10.6667 11.8333V4.16667C10.6667 4.07462 10.592 4 10.5 4H6.83333Z" fill="#0033FF"/> |
| 398 | + </svg> |
| 399 | + <h1>MCP Server authenticated</h1> |
| 400 | + <p>You can close this tab</p> |
| 401 | + </div> |
| 402 | + <script>setTimeout(() => window.close(), 1000)</script> |
| 403 | +</body> |
| 404 | +</html>`) |
| 405 | + } else { |
| 406 | + res.writeHead(400, { "Content-Type": "text/plain" }) |
| 407 | + res.end("Missing code or state parameter") |
| 408 | + } |
315 | 409 | } else { |
316 | 410 | res.writeHead(404, { "Content-Type": "text/plain" }) |
317 | 411 | res.end("Not found") |
318 | 412 | } |
319 | 413 | }) |
320 | 414 |
|
321 | | - server.listen(21321, () => { |
322 | | - console.log("[Auth Server] Listening on http://localhost:21321") |
323 | | - }) |
324 | | -} |
| 415 | +server.listen(21321, () => { |
| 416 | + console.log("[Auth Server] Listening on http://localhost:21321") |
| 417 | +}) |
325 | 418 |
|
326 | 419 | // Clean up stale lock files from crashed instances |
327 | 420 | // Returns true if locks were cleaned, false otherwise |
@@ -670,6 +763,7 @@ if (gotTheLock) { |
670 | 763 | // Cleanup before quit |
671 | 764 | app.on("before-quit", async () => { |
672 | 765 | console.log("[App] Shutting down...") |
| 766 | + cancelAllPendingOAuth() |
673 | 767 | await cleanupGitWatchers() |
674 | 768 | await shutdownAnalytics() |
675 | 769 | await closeDatabase() |
|
0 commit comments