oauth migration for new scopes

This commit is contained in:
Arjun 2026-05-28 15:26:04 +05:30
parent 537b6f66bb
commit 143c4f83ec
2 changed files with 51 additions and 1 deletions

View file

@ -51,6 +51,7 @@ import {
extractDeepLinkFromArgv,
setMainWindowForDeepLinks,
} from "./deeplink.js";
import { disconnectGoogleIfScopesStale } from "./oauth-handler.js";
const execAsync = promisify(exec);
@ -351,6 +352,11 @@ app.whenReady().then(async () => {
registerConsumer(backgroundTaskEventConsumer);
initEventProcessor();
// If the stored Google grant predates a scope change (only old scopes),
// disconnect it now so the user re-connects with the current scopes before
// any Google sync runs against the stale grant.
await disconnectGoogleIfScopesStale();
// start gmail sync
initGmailSync();

View file

@ -508,7 +508,7 @@ export async function disconnectProvider(provider: string): Promise<{ success: b
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' });
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 disconnect`);
}
@ -532,6 +532,50 @@ 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.
*
* 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.
*/
export async function disconnectGoogleIfScopesStale(): Promise<void> {
try {
const oauthRepo = getOAuthRepo();
const connection = await oauthRepo.read('google');
// Not connected — nothing to migrate.
if (!connection.tokens) {
return;
}
const providerConfig = await getProviderConfig('google');
const requiredScopes = providerConfig.scopes ?? [];
if (requiredScopes.length === 0) {
return;
}
const granted = new Set(connection.tokens.scopes ?? []);
const missingScopes = requiredScopes.filter((scope) => !granted.has(scope));
if (missingScopes.length === 0) {
return;
}
console.log(
`[OAuth] Google grant is missing current scopes [${missingScopes.join(', ')}]; ` +
'disconnecting so the user can reconnect with the new scopes.'
);
await disconnectProvider('google');
} catch (error) {
console.error('[OAuth] Google scope migration check failed:', error);
}
}
/**
* Get access token for a provider (internal use only)
* Refreshes token if expired