feat(google-docs): use Google Picker + drive.file scope instead of full-drive listing

This commit is contained in:
Gagancreates 2026-06-01 23:47:27 +05:30
parent 505a9a27e8
commit d08bf49d5a
6 changed files with 229 additions and 174 deletions

View file

@ -77,10 +77,10 @@ const providerConfigs: ProviderConfig = {
scopes: [
'https://www.googleapis.com/auth/gmail.modify',
'https://www.googleapis.com/auth/calendar.events.readonly',
// Full Drive access: read/export Google Docs to .docx AND write the edited
// .docx back into the original doc (files.update needs write, which
// drive.readonly does not grant). Covers list/get/export/update.
'https://www.googleapis.com/auth/drive',
// Per-file Drive access (non-restricted): the user grants read/write to a
// specific doc by choosing it in the Google Picker. Enough to export/
// download and write back, without the restricted full-drive scope.
'https://www.googleapis.com/auth/drive.file',
],
},
'fireflies-ai': {

View file

@ -5,10 +5,11 @@ import { google, drive_v3 as drive } from 'googleapis';
import { resolveWorkspacePath } from '../workspace/workspace.js';
import { GoogleClientFactory } from './google-client-factory.js';
// Full Drive scope: export Google Docs to .docx (read) and write the edited
// .docx back via files.update (write). drive.readonly can't do the write half.
// Per-file Drive scope (non-restricted). The user picks a doc via the Google
// Picker, which grants this app read/write to that file — enough to export/
// download it and write edits back, without the restricted full-drive scope.
export const GOOGLE_DOC_SCOPES = [
'https://www.googleapis.com/auth/drive',
'https://www.googleapis.com/auth/drive.file',
] as const;
export type GoogleDocListItem = {
@ -58,10 +59,6 @@ function sanitizeFilename(name: string): string {
return cleaned || 'Google Doc';
}
function escapeDriveQueryValue(value: string): string {
return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
}
function normalizeRel(relPath: string): string {
return relPath.replace(/\\/g, '/');
}
@ -198,34 +195,13 @@ export async function getGoogleDocsConnectionStatus(): Promise<{
return GoogleClientFactory.getCredentialStatus([...GOOGLE_DOC_SCOPES]);
}
export async function listGoogleDocs(query?: string): Promise<{ files: GoogleDocListItem[] }> {
const status = await getGoogleDocsConnectionStatus();
if (!status.connected) throw new Error('Google is not connected.');
if (!status.hasRequiredScopes) throw new Error('Google is missing Drive access. Reconnect Google.');
const driveClient = await getDriveClient();
// Native Google Docs (exportable) and uploaded Word files (downloadable).
const typeClause = `(mimeType='${GOOGLE_DOC_MIME}' or mimeType='${DOCX_MIME}')`;
const clauses = [typeClause, 'trashed=false'];
const trimmed = query?.trim();
if (trimmed) {
clauses.push(`name contains '${escapeDriveQueryValue(trimmed)}'`);
}
const q = clauses.join(' and ');
const result = await driveClient.files.list({
q,
pageSize: 25,
orderBy: 'modifiedTime desc',
fields: 'files(id,name,webViewLink,modifiedTime,mimeType,owners(displayName,emailAddress))',
// Also surface docs in shared drives and "Shared with me", not just My Drive.
corpora: 'allDrives',
includeItemsFromAllDrives: true,
supportsAllDrives: true,
});
const files = (result.data.files ?? []).map(toGoogleDocListItem).filter((file) => file.id);
console.log(`[GoogleDocs] list q="${q}" → ${files.length} doc(s)`);
return { files };
/**
* The live Google OAuth access token, for the renderer to drive the Google
* Picker (file selection happens client-side; the app never lists Drive).
*/
export async function getGoogleAccessToken(): Promise<string | null> {
const auth = await GoogleClientFactory.getClient();
return auth?.credentials?.access_token ?? null;
}
/** Import a Google Doc as a local .docx and register the link. */