From a70e7d1b02675e496030bfa4e87f6bb22a0d1965 Mon Sep 17 00:00:00 2001 From: Arjun <6592213+arkml@users.noreply.github.com> Date: Thu, 28 May 2026 15:59:09 +0530 Subject: [PATCH] trigger google reconnect popover --- apps/x/apps/main/src/oauth-handler.ts | 47 ++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/apps/x/apps/main/src/oauth-handler.ts b/apps/x/apps/main/src/oauth-handler.ts index a4ab33f7..1048d9b8 100644 --- a/apps/x/apps/main/src/oauth-handler.ts +++ b/apps/x/apps/main/src/oauth-handler.ts @@ -535,21 +535,29 @@ export async function disconnectProvider(provider: string): Promise<{ success: b /** * Startup migration for Google scope changes. When a connected Google grant was * issued before a scope was added (e.g. old installs on gmail.readonly that - * never received gmail.modify), disconnect it so the renderer re-prompts the - * user through the normal connect flow and they re-grant with the current - * scopes. The currently-requested scopes in the provider config are the source - * of truth: a grant missing any of them is treated as stale. + * never received gmail.modify), invalidate it so the user is prompted to + * reconnect and re-grant with the current scopes. The currently-requested + * scopes in the provider config are the source of truth: a grant missing any + * of them is treated as stale. + * + * We revoke + clear the stale token but DELIBERATELY keep the provider entry + * with an `error` set rather than calling disconnectProvider (which deletes the + * whole entry). The renderer's reconnect prompts — the sidebar "Reconnect your + * accounts" alert and the connectors "Reconnect" row — key off this `error` + * field, not off the connected flag. A fully deleted entry has no error and is + * indistinguishable from "never connected", so no prompt would ever appear. * * Tokens with no recorded scopes (very old installs that never persisted them) * are also treated as stale. Safe to call on every startup — it's a no-op once - * the grant covers all current scopes. + * the grant covers all current scopes, and once invalidated the early return on + * the missing token keeps it from re-running until the user reconnects. */ export async function disconnectGoogleIfScopesStale(): Promise { try { const oauthRepo = getOAuthRepo(); const connection = await oauthRepo.read('google'); - // Not connected — nothing to migrate. + // Not connected (or already invalidated) — nothing to migrate. if (!connection.tokens) { return; } @@ -568,9 +576,32 @@ export async function disconnectGoogleIfScopesStale(): Promise { console.log( `[OAuth] Google grant is missing current scopes [${missingScopes.join(', ')}]; ` + - 'disconnecting so the user can reconnect with the new scopes.' + 'invalidating it so the user is prompted to reconnect with the new scopes.' ); - await disconnectProvider('google'); + + // Best-effort revoke at Google for rowboat-mode grants (mirrors disconnectProvider). + if (connection.mode === 'rowboat' && connection.tokens.access_token) { + try { + const revokeUrl = `https://oauth2.googleapis.com/revoke?token=${encodeURIComponent(connection.tokens.access_token)}`; + const res = await fetch(revokeUrl, { method: 'POST', signal: AbortSignal.timeout(5000) }); + if (!res.ok) { + console.warn(`[OAuth] Google revoke returned ${res.status}; continuing with local invalidation`); + } + } catch (error) { + console.warn('[OAuth] Google revoke failed; continuing with local invalidation:', error); + } + } + + // Drop the stale token but keep the entry with an error so the reconnect + // prompt fires (see the note above). + await oauthRepo.upsert('google', { + tokens: null, + error: 'Google permissions changed. Please reconnect to continue.', + }); + + // Nudge any already-open window to re-read state. The renderer's initial + // mount also re-reads, so the prompt shows even if no window is up yet. + emitOAuthEvent({ provider: 'google', success: false }); } catch (error) { console.error('[OAuth] Google scope migration check failed:', error); }