Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

234 changes: 10 additions & 224 deletions src/main/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import * as Sentry from "@sentry/electron/main"
import { app, BrowserWindow, dialog, Menu, nativeImage, session } from "electron"
import { app, BrowserWindow, dialog, Menu, nativeImage } from "electron"
import { existsSync, readFileSync, readlinkSync, unlinkSync } from "fs"
import { createServer } from "http"
import { join } from "path"
import { AuthManager, initAuthManager, getAuthManager as getAuthManagerFromModule } from "./auth-manager"

import {
identify,
initAnalytics,
setSubscriptionPlan,
shutdown as shutdownAnalytics,
trackAppOpened,
trackAuthCompleted,
} from "./lib/analytics"
import {
checkForUpdates,
Expand All @@ -28,6 +26,10 @@ import {
} from "./lib/cli"
import { cleanupGitWatchers } from "./lib/git/watcher"
import { cancelAllPendingOAuth, handleMcpOAuthCallback } from "./lib/mcp-auth"
// Re-export auth manager for tRPC routers that may need it
// Note: auth manager is no longer auto-initialized since 21st.dev auth is removed.
// getAuthManager() returns null — routers should handle this gracefully.
export { getAuthManager } from "./auth-manager"
import { getAllMcpConfigHandler, hasActiveClaudeSessions, abortAllClaudeSessions } from "./lib/trpc/routers/claude"
import { getAllCodexMcpConfigHandler, hasActiveCodexStreams, abortAllCodexStreams } from "./lib/trpc/routers/codex"
import {
Expand Down Expand Up @@ -91,116 +93,15 @@ export function getAppUrl(): string {
return process.env.ELECTRON_RENDERER_URL || "https://21st.dev/agents"
}

// Auth manager singleton (use the one from auth-manager module)
let authManager: AuthManager

export function getAuthManager(): AuthManager {
// First try to get from module, fallback to local variable for backwards compat
return getAuthManagerFromModule() || authManager
}

// Handle auth code from deep link (exported for IPC handlers)
export async function handleAuthCode(code: string): Promise<void> {
console.log("[Auth] Handling auth code:", code.slice(0, 8) + "...")

try {
const authData = await authManager.exchangeCode(code)
console.log("[Auth] Success for user:", authData.user.email)

// Track successful authentication
trackAuthCompleted(authData.user.id, authData.user.email)

// Fetch and set subscription plan for analytics
try {
const planData = await authManager.fetchUserPlan()
if (planData) {
setSubscriptionPlan(planData.plan)
}
} catch (e) {
console.warn("[Auth] Failed to fetch user plan for analytics:", e)
}

// Set desktop token cookie using persist:main partition
const ses = session.fromPartition("persist:main")
try {
// First remove any existing cookie to avoid HttpOnly conflict
await ses.cookies.remove(getBaseUrl(), "x-desktop-token")
await ses.cookies.set({
url: getBaseUrl(),
name: "x-desktop-token",
value: authData.token,
expirationDate: Math.floor(
new Date(authData.expiresAt).getTime() / 1000,
),
httpOnly: false,
secure: getBaseUrl().startsWith("https"),
sameSite: "lax" as const,
})
console.log("[Auth] Desktop token cookie set")
} catch (cookieError) {
// Cookie setting is optional - auth data is already saved to disk
console.warn("[Auth] Cookie set failed (non-critical):", cookieError)
}

// Notify all windows and reload them to show app
const windows = getAllWindows()
for (const win of windows) {
try {
if (win.isDestroyed()) continue
win.webContents.send("auth:success", authData.user)

// Use stable window ID (main, window-2, etc.) instead of Electron's numeric ID
const stableId = windowManager.getStableId(win)

if (process.env.ELECTRON_RENDERER_URL) {
// Pass window ID via query param for dev mode
const url = new URL(process.env.ELECTRON_RENDERER_URL)
url.searchParams.set("windowId", stableId)
win.loadURL(url.toString())
} else {
// Pass window ID via hash for production
win.loadFile(join(__dirname, "../renderer/index.html"), {
hash: `windowId=${stableId}`,
})
}
} catch (error) {
// Window may have been destroyed during iteration
console.warn("[Auth] Failed to reload window:", error)
}
}
// Focus the first window
windows[0]?.focus()
} catch (error) {
console.error("[Auth] Exchange failed:", error)
// Broadcast auth error to all windows (not just focused)
for (const win of getAllWindows()) {
try {
if (!win.isDestroyed()) {
win.webContents.send("auth:error", (error as Error).message)
}
} catch {
// Window destroyed during iteration
}
}
}
}

// Handle deep link
// Handle deep link (only MCP OAuth now, no account auth)
function handleDeepLink(url: string): void {
console.log("[DeepLink] Received:", url)

try {
const parsed = new URL(url)

// Handle auth callback: twentyfirst-agents://auth?code=xxx
if (parsed.pathname === "/auth" || parsed.host === "auth") {
const code = parsed.searchParams.get("code")
if (code) {
handleAuthCode(code)
return
}
}

// Handle MCP OAuth callback: twentyfirst-agents://mcp-oauth?code=xxx&state=yyy
if (parsed.pathname === "/mcp-oauth" || parsed.host === "mcp-oauth") {
const code = parsed.searchParams.get("code")
Expand Down Expand Up @@ -299,87 +200,7 @@ const server = createServer((req, res) => {
return
}

if (url.pathname === "/auth/callback") {
const code = url.searchParams.get("code")
console.log(
"[Auth Server] Received callback with code:",
code?.slice(0, 8) + "...",
)

if (code) {
// Handle the auth code
handleAuthCode(code)

// Send success response and close the browser tab
res.writeHead(200, { "Content-Type": "text/html" })
res.end(`<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link rel="icon" type="image/svg+xml" href="${FAVICON_DATA_URI}">
<title>1Code - Authentication</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg: #09090b;
--text: #fafafa;
--text-muted: #71717a;
}
@media (prefers-color-scheme: light) {
:root {
--bg: #ffffff;
--text: #09090b;
--text-muted: #71717a;
}
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
background: var(--bg);
color: var(--text);
}
.container {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.logo {
width: 24px;
height: 24px;
margin-bottom: 8px;
}
h1 {
font-size: 14px;
font-weight: 500;
margin-bottom: 4px;
}
p {
font-size: 12px;
color: var(--text-muted);
}
</style>
</head>
<body>
<div class="container">
<svg class="logo" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<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"/>
</svg>
<h1>Authentication successful</h1>
<p>You can close this tab</p>
</div>
<script>setTimeout(() => window.close(), 1000)</script>
</body>
</html>`)
} else {
res.writeHead(400, { "Content-Type": "text/plain" })
res.end("Missing code parameter")
}
} else if (url.pathname === "/callback") {
if (url.pathname === "/callback") {
// Handle MCP OAuth callback
const code = url.searchParams.get("code")
const state = url.searchParams.get("state")
Expand Down Expand Up @@ -890,47 +711,12 @@ if (gotTheLock) {
// Build initial menu
buildMenu()

// Initialize auth manager (uses singleton from auth-manager module)
authManager = initAuthManager(!!process.env.ELECTRON_RENDERER_URL)
console.log("[App] Auth manager initialized")

// Initialize analytics after auth manager so we can identify user
// Initialize analytics
initAnalytics()

// If user already authenticated from previous session, identify them
if (authManager.isAuthenticated()) {
const user = authManager.getUser()
if (user) {
identify(user.id, { email: user.email })
console.log("[Analytics] User identified from saved session:", user.id)
}
}

// Track app opened (now with correct user ID if authenticated)
// Track app opened
trackAppOpened()

// Set up callback to update cookie when token is refreshed
authManager.setOnTokenRefresh(async (authData) => {
console.log("[Auth] Token refreshed, updating cookie...")
const ses = session.fromPartition("persist:main")
try {
await ses.cookies.set({
url: getBaseUrl(),
name: "x-desktop-token",
value: authData.token,
expirationDate: Math.floor(
new Date(authData.expiresAt).getTime() / 1000,
),
httpOnly: false,
secure: getBaseUrl().startsWith("https"),
sameSite: "lax" as const,
})
console.log("[Auth] Desktop token cookie updated after refresh")
} catch (err) {
console.error("[Auth] Failed to update cookie:", err)
}
})

// Initialize database
try {
initDatabase()
Expand Down
Loading