Skip to content

Commit fb6ea79

Browse files
committed
fix(google-docs): managed picker downloads with its own drive.file token
The managed Google connect doesn't grant drive.file, so the picked file can't be read with the user's main token. The picker runs a standalone drive.file authorization, so claim-picked now returns { fileIds, tokens } and the desktop downloads with that fresh token via importGoogleDocWithToken — matching the backend contract and keeping the picker self-contained (no forced reconnect).
1 parent 3316f30 commit fb6ea79

2 files changed

Lines changed: 21 additions & 14 deletions

File tree

apps/x/apps/main/src/google-picker-managed.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { shell, BrowserWindow } from 'electron';
22
import { getWebappUrl } from '@x/core/dist/config/remote-config.js';
33
import { claimPickedFilesViaBackend } from '@x/core/dist/auth/google-backend-oauth.js';
4-
import { importGoogleDoc } from '@x/core/dist/knowledge/google_docs.js';
4+
import { importGoogleDocWithToken } from '@x/core/dist/knowledge/google_docs.js';
55
import type { GoogleDocListItem } from '@x/core/dist/knowledge/google_docs.js';
66

77
// Managed (rowboat-mode) OAuth-redirect Picker. Unlike BYOK, the OAuth runs on
@@ -27,7 +27,9 @@ import type { GoogleDocListItem } from '@x/core/dist/knowledge/google_docs.js';
2727
// client, so the desktop's existing managed token can read it.)
2828
//
2929
// POST ${API_URL}/v1/google-oauth/claim-picked body { session }
30-
// Authenticated with the user's Rowboat bearer. Returns { fileIds: string[] }.
30+
// Authenticated with the user's Rowboat bearer. Returns
31+
// { fileIds: string[], tokens: { access_token, ... } } — a fresh
32+
// drive.file token minted during the picker's own authorization.
3133

3234
export interface ManagedPickResult {
3335
path: string;
@@ -103,14 +105,14 @@ export async function completeManagedGooglePick(session: string): Promise<void>
103105
focusApp();
104106

105107
try {
106-
const fileIds = await claimPickedFilesViaBackend(session);
107-
if (fileIds.length === 0) {
108+
const { fileIds, accessToken } = await claimPickedFilesViaBackend(session);
109+
if (fileIds.length === 0 || !accessToken) {
108110
current.resolve(null);
109111
return;
110112
}
111-
// drive.file is granted to the company client, so the user's existing
112-
// managed token reads the picked file — the normal import path applies.
113-
const result = await importGoogleDoc(fileIds[0], current.targetFolder);
113+
// Download with the picker's own fresh drive.file token (the main
114+
// connection doesn't carry drive.file).
115+
const result = await importGoogleDocWithToken(fileIds[0], current.targetFolder, accessToken);
114116
current.resolve(result);
115117
} catch (error) {
116118
current.reject(error instanceof Error ? error : new Error(String(error)));

apps/x/packages/core/src/auth/google-backend-oauth.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,21 +108,26 @@ export async function claimTokensViaBackend(state: string): Promise<OAuthTokens>
108108
}
109109

110110
/**
111-
* Claim the Drive file ids the user selected in the managed OAuth-redirect
112-
* Picker, parked under `session` by the webapp picker callback. Mirrors
113-
* claimTokensViaBackend — the picker hands back file ids, not tokens, because
114-
* the desktop already holds a managed token that can read the picked files.
111+
* Claim what the user selected in the managed OAuth-redirect Picker, parked
112+
* under `session` by the webapp picker callback. Returns the picked file ids
113+
* plus a fresh drive.file access token — the picker runs a standalone
114+
* drive.file authorization (the main connection doesn't carry drive.file), so
115+
* the desktop downloads the picked files with this token, not the main one.
115116
*/
116-
export async function claimPickedFilesViaBackend(session: string): Promise<string[]> {
117+
export async function claimPickedFilesViaBackend(
118+
session: string,
119+
): Promise<{ fileIds: string[]; accessToken: string }> {
117120
const res = await postWithBearer("/v1/google-oauth/claim-picked", { session });
118121
if (!res.ok) {
119122
const err = await readError(res);
120123
throw new Error(`claim picked files failed: ${res.status} ${err.error ?? ""}`.trim());
121124
}
122-
const body = (await res.json()) as { fileIds?: unknown };
123-
return Array.isArray(body.fileIds)
125+
const body = (await res.json()) as { fileIds?: unknown; tokens?: { access_token?: unknown } };
126+
const fileIds = Array.isArray(body.fileIds)
124127
? body.fileIds.filter((id): id is string => typeof id === "string" && id.length > 0)
125128
: [];
129+
const accessToken = typeof body.tokens?.access_token === "string" ? body.tokens.access_token : "";
130+
return { fileIds, accessToken };
126131
}
127132

128133
/**

0 commit comments

Comments
 (0)