Skip to content
Merged
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
2 changes: 1 addition & 1 deletion packages/core/src/client/CommandMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ export function CommandMenu({
<div
ref={containerRef}
className={cn(
"fixed left-1/2 top-[10vh] -translate-x-1/2 w-full max-w-lg",
"fixed left-1/2 top-[5vh] -translate-x-1/2 w-full max-w-lg",
"rounded-lg border border-border bg-popover text-popover-foreground shadow-lg",
className,
)}
Expand Down
25 changes: 15 additions & 10 deletions packages/core/src/server/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,19 +206,24 @@ export async function getSession(event: H3Event): Promise<AuthSession | null> {
if (isDevMode() || authDisabledMode) {
// Check for a real session cookie (created by Google OAuth callback)
// so dev and prod share the same identity on the same DB
const cookie = getCookie(event, COOKIE_NAME);
if (cookie) {
const email = await getSessionEmail(cookie);
if (email) return { email, token: cookie };
try {
const cookie = getCookie(event, COOKIE_NAME);
if (cookie) {
const email = await getSessionEmail(cookie);
if (email) return { email, token: cookie };
}
} catch {
// DB not ready yet — fall back to dev session
}
return DEV_SESSION;
}

if (customGetSession) return customGetSession(event);

const cookie = getCookie(event, COOKIE_NAME);
if (cookie && (await hasSession(cookie))) {
return { email: "user", token: cookie };
if (cookie) {
const email = await getSessionEmail(cookie);
if (email) return { email, token: cookie };
Comment on lines 223 to +226
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Production auth broken: ACCESS_TOKEN sessions have null email, causing infinite login loop

The mountAuthRoutes login handler calls await addSession(sessionToken) without an email, storing email = NULL in the DB. getSessionEmail returns null for NULL emails (line 135: (rows[0].email as string) ?? null), so this block never returns a session and falls through to return null — causing every ACCESS_TOKEN-authenticated request to be treated as unauthenticated. Fix: await addSession(sessionToken, "user") in the login handler to match pre-PR behavior.


How did I do? React with 👍 or 👎 to help me improve.

}
return null;
}
Expand Down Expand Up @@ -497,15 +502,15 @@ export function autoMountAuth(app: H3App, options: AuthOptions = {}): boolean {

// Dev mode — skip auth entirely
if (isDevMode()) {
// Mount a session endpoint that returns the dev stub
// Mount a session endpoint that checks for a real session first
app.use(
"/api/auth/session",
defineEventHandler(async (event) => {
if (getMethod(event) !== "GET") {
setResponseStatus(event, 405);
return { error: "Method not allowed" };
}
return DEV_SESSION;
return await getSession(event);
}),
);

Expand Down Expand Up @@ -596,15 +601,15 @@ export function autoMountAuth(app: H3App, options: AuthOptions = {}): boolean {
"Ensure this app is behind infrastructure-level auth (Cloudflare Access, VPN, etc.).",
);

// Mount session endpoint — getSession() will return DEV_SESSION
// Mount session endpoint
app.use(
"/api/auth/session",
defineEventHandler(async (event) => {
if (getMethod(event) !== "GET") {
setResponseStatus(event, 405);
return { error: "Method not allowed" };
}
return DEV_SESSION;
return await getSession(event);
}),
);
app.use(
Expand Down
65 changes: 58 additions & 7 deletions packages/desktop-app/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,20 +199,57 @@ ipcMain.on(IPC.INTER_APP_SEND, (event: IpcMainEvent, msg: InterAppMessage) => {
});
});

// ---------- OAuth popup handling ----------
// Open OAuth flows in an Electron BrowserWindow so the callback stays
// inside the app. Other popups still open in the system browser.

const OAUTH_HOSTS = ["accounts.google.com"];

function openAuthWindow(url: string) {
const authWin = new BrowserWindow({
width: 500,
height: 680,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
},
});

authWin.loadURL(url);

// After the OAuth provider redirects to localhost (the callback),
// let the request complete then close the popup.
authWin.webContents.on("did-navigate", (_event, navUrl) => {
try {
const parsed = new URL(navUrl);
if (parsed.hostname === "localhost") {
Comment on lines +220 to +225
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 OAuth popup never auto-closes for hosted production app callbacks

openAuthWindow only closes when parsed.hostname === "localhost", but the desktop app's default production URLs (https://mail.agent-native.com, etc.) redirect OAuth callbacks to the hosted origin — so the BrowserWindow stays open after successful sign-in. Fix: detect the callback path (e.g. /api/google/callback) regardless of hostname instead of hard-coding localhost.


How did I do? React with 👍 or 👎 to help me improve.

// Give the callback handler time to store tokens then close
setTimeout(() => {
if (!authWin.isDestroyed()) authWin.close();
}, 1500);
}
} catch {
// ignore
}
});
}

// ---------- Webview popup handling ----------
// Open popups from webviews (e.g. OAuth flows) in the system browser
// instead of creating broken Electron popup windows.

app.on("web-contents-created", (_event, contents) => {
// Only intercept webview guest contents
if (contents.getType() !== "webview") return;

contents.setWindowOpenHandler(({ url }) => {
// Only allow http/https URLs to prevent protocol-handler attacks
// (e.g. ms-msdt:, file://, etc.)
try {
const parsed = new URL(url);
if (parsed.protocol === "https:" || parsed.protocol === "http:") {
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
return { action: "deny" };
}
// OAuth flows stay in Electron so the callback redirect works
if (OAUTH_HOSTS.includes(parsed.hostname)) {
openAuthWindow(url);
} else {
shell.openExternal(url);
}
} catch {
Expand Down Expand Up @@ -245,9 +282,13 @@ app.on("web-contents-created", (_event, contents) => {
return;
}

// Forward other Cmd+ shortcuts: T, Shift+T, 1-9, [, ]
// Forward other Cmd+ shortcuts: R, T, Shift+T, 1-9, [, ]
const isShortcut =
key === "t" || key === "[" || key === "]" || (key >= "1" && key <= "9");
key === "r" ||
key === "t" ||
key === "[" ||
key === "]" ||
(key >= "1" && key <= "9");

if (isShortcut) {
event.preventDefault();
Expand Down Expand Up @@ -311,6 +352,16 @@ app.whenReady().then(() => {
return;
}

// Cmd+R — refresh active webview, not the shell
if (key === "r") {
_event.preventDefault();
win.webContents.send("shortcut:keydown", {
key: "r",
shiftKey: input.shift,
});
return;
}

// Cmd+W — close tab instead of window
if (key === "w") {
_event.preventDefault();
Expand Down
7 changes: 7 additions & 0 deletions packages/desktop-app/src/renderer/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export default function App() {
}, [enabledApps.map((a) => a.id).join(",")]);

const closedTabsRef = useRef<{ tab: Tab; appId: string }[]>([]);
const [refreshKey, setRefreshKey] = useState(0);

const currentAppTabs = appTabs[activeSidebarAppId];

Expand Down Expand Up @@ -196,6 +197,11 @@ export default function App() {
(key: string, shiftKey: boolean) => {
const k = key.toLowerCase();

if (k === "r") {
setRefreshKey((n) => n + 1);
return;
}

if (k === "t") {
if (shiftKey) handleReopenTab();
else handleNewTab();
Expand Down Expand Up @@ -314,6 +320,7 @@ export default function App() {
app={appDef}
appConfig={app}
isActive={isActive}
refreshKey={isActive ? refreshKey : 0}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 refreshKey resets inactive webviews to 0, causing unintended reload on tab switch

Inactive AppWebviews receive refreshKey=0 while the global counter increments on Cmd+R. When the user later switches to an inactive tab, the prop jumps from 0 to the current counter value, triggering reloadIgnoringCache() in AppWebview's effect. Fix: pass the real refreshKey to all webviews and rely solely on the isActive guard inside AppWebview to prevent unwanted reloads.


How did I do? React with 👍 or 👎 to help me improve.

/>
))}
</div>
Expand Down
19 changes: 19 additions & 0 deletions packages/desktop-app/src/renderer/components/AppWebview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ interface AppWebviewProps {
/** Full app config with URL overrides (optional for backward compat) */
appConfig?: AppConfig;
isActive: boolean;
/** Increment to trigger a webview reload (Cmd+R) */
refreshKey?: number;
}

/**
Expand Down Expand Up @@ -42,6 +44,7 @@ export default function AppWebview({
app,
appConfig,
isActive,
refreshKey = 0,
}: AppWebviewProps) {
const webviewRef = useRef<ElectronWebviewElement>(null);
const [error, setError] = useState(false);
Expand Down Expand Up @@ -122,6 +125,22 @@ export default function AppWebview({
};
}, [app.placeholder, isActive, app.id]);

// Cmd+R — reload the active webview when refreshKey increments
const prevRefreshKey = useRef(refreshKey);
useEffect(() => {
if (refreshKey > 0 && refreshKey !== prevRefreshKey.current) {
prevRefreshKey.current = refreshKey;
const wv = webviewRef.current;
if (wv && isActive && !app.placeholder) {
try {
wv.reloadIgnoringCache();
} catch {
wv.reload();
}
}
}
}, [refreshKey, isActive, app.placeholder]);

useEffect(() => {
if (isActive && error && !app.placeholder) {
handleRetry();
Expand Down
Loading
Loading