gmail proxy tool with token refresh, claude pro bearer auth, pi --list-models stderr fallback, tool object input type
- add getValidGoogleAccessToken helper and use it in email-cron, email account auth resolver, and the new gmail proxy - POST /api/integrations/google/gmail-proxy forwards arbitrary gmail rest calls server-side, with auto-refreshed oauth - pi-manager and claude user-instance inject OFFICER_API_URL + per-session JWT so tools can call back as the user - claude anthropic proxy uses Authorization: Bearer + preserves any anthropic-beta headers (pro oauth tokens are rejected via x-api-key, and overwriting the beta header broke context_management) - pi --list-models: fall back to stderr when stdout is empty (pi v0.73.1 writes the table to stderr) - mcp tool server + pi tool loader: accept type: object inputs so json bodies stay structured Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,9 +5,9 @@ import {
|
||||
getEmailAccount,
|
||||
createEmailAccount,
|
||||
deleteEmailAccount,
|
||||
getUserIntegration,
|
||||
updateEmailAccountStatus,
|
||||
} from 'officerdb';
|
||||
import { getValidGoogleAccessToken } from '../integrations/google-auth';
|
||||
import { validateImapConnection } from './imap-validate';
|
||||
import { enqueueJob, listAllJobs } from '../../queue/init';
|
||||
import { openEmailDb, getSyncMeta } from './email-db';
|
||||
@@ -237,11 +237,12 @@ async function resolveAuth(
|
||||
const integrationId = credentials.userIntegrationId as number | undefined;
|
||||
if (!integrationId) return { ok: false, error: 'Missing userIntegrationId for OAuth' };
|
||||
|
||||
const integration = await getUserIntegration(userId, 'google');
|
||||
if (!integration) return { ok: false, error: 'Google integration not found' };
|
||||
|
||||
const config = integration.config as Record<string, unknown>;
|
||||
const accessToken = config.accessToken as string | undefined;
|
||||
let accessToken: string | null;
|
||||
try {
|
||||
accessToken = await getValidGoogleAccessToken(userId);
|
||||
} catch (err) {
|
||||
return { ok: false, error: `Token refresh failed — reconnect Google account: ${err instanceof Error ? err.message : err}` };
|
||||
}
|
||||
if (!accessToken) return { ok: false, error: 'No access token available — reconnect Google account' };
|
||||
|
||||
return { ok: true, auth: { accessToken } };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getServerIntegration } from 'officerdb';
|
||||
import { getServerIntegration, getUserIntegration, upsertUserIntegration } from 'officerdb';
|
||||
import { PermanentError } from '../../queue/types';
|
||||
|
||||
type TokenRefreshResult = { accessToken: string; expiresAt: number };
|
||||
@@ -32,3 +32,30 @@ export async function refreshGoogleAccessToken(refreshToken: string): Promise<To
|
||||
expiresAt: Date.now() + data.expires_in * 1000,
|
||||
};
|
||||
}
|
||||
|
||||
// Returns a non-expired Google access token for the user, refreshing and
|
||||
// persisting a new one when the stored token is within 60s of expiry.
|
||||
// Returns null if the user has no stored token to work with.
|
||||
export async function getValidGoogleAccessToken(userId: number): Promise<string | null> {
|
||||
const integration = await getUserIntegration(userId, 'google');
|
||||
const config = integration?.config as Record<string, unknown> | undefined;
|
||||
if (!config) return null;
|
||||
|
||||
const accessToken = config.accessToken as string | undefined;
|
||||
const refreshToken = config.refreshToken as string | undefined;
|
||||
const expiresAt = config.expiresAt as number | undefined;
|
||||
if (!accessToken) return null;
|
||||
|
||||
const expired = !expiresAt || expiresAt < Date.now() + 60_000;
|
||||
if (!expired || !refreshToken) return accessToken;
|
||||
|
||||
const refreshed = await refreshGoogleAccessToken(refreshToken);
|
||||
const serverGoogle = await getServerIntegration('google');
|
||||
await upsertUserIntegration({
|
||||
userId,
|
||||
provider: 'google',
|
||||
serverIntegrationId: serverGoogle?.id,
|
||||
config: { ...config, accessToken: refreshed.accessToken, expiresAt: refreshed.expiresAt },
|
||||
});
|
||||
return refreshed.accessToken;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
getDockPaths,
|
||||
setDockPaths,
|
||||
} from 'officerdb';
|
||||
import { getValidGoogleAccessToken } from './google-auth';
|
||||
|
||||
const GOOGLE_SCOPES = [
|
||||
'https://mail.google.com/',
|
||||
@@ -166,6 +167,41 @@ integrationsRouter.delete('/google/connection', async (ctx) => {
|
||||
return ctx.json({ ok: true });
|
||||
});
|
||||
|
||||
// --- Gmail proxy: forwards arbitrary Gmail REST calls with auto-refreshed OAuth ---
|
||||
|
||||
type GmailProxyBody = { method?: string; path?: string; body?: unknown };
|
||||
const GMAIL_ALLOWED_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']);
|
||||
|
||||
integrationsRouter.post('/google/gmail-proxy', async (ctx) => {
|
||||
const user = ctx.get('user');
|
||||
const body = ctx.get('body') as GmailProxyBody;
|
||||
|
||||
const method = (body.method ?? '').toUpperCase();
|
||||
const path = body.path ?? '';
|
||||
if (!GMAIL_ALLOWED_METHODS.has(method)) throw BAD_REQUEST('Invalid method');
|
||||
if (!path.startsWith('/')) throw BAD_REQUEST('path must start with /');
|
||||
|
||||
const accessToken = await getValidGoogleAccessToken(user.id);
|
||||
if (!accessToken) throw BAD_REQUEST('No Google account connected — connect in Settings → Integrations');
|
||||
|
||||
const url = `https://gmail.googleapis.com/gmail/v1${path}`;
|
||||
const init: RequestInit = {
|
||||
method,
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
};
|
||||
if (body.body !== undefined && method !== 'GET' && method !== 'DELETE') {
|
||||
(init.headers as Record<string, string>)['Content-Type'] = 'application/json';
|
||||
init.body = JSON.stringify(body.body);
|
||||
}
|
||||
|
||||
const upstream = await fetch(url, init);
|
||||
const text = await upstream.text();
|
||||
let parsed: unknown;
|
||||
try { parsed = JSON.parse(text); } catch { parsed = text; }
|
||||
|
||||
return ctx.json({ status: upstream.status, ok: upstream.ok, body: parsed });
|
||||
});
|
||||
|
||||
// --- OAuth flow: authorize (protected — user must be logged in) ---
|
||||
|
||||
integrationsRouter.get('/google/authorize', async (ctx) => {
|
||||
|
||||
@@ -50,7 +50,7 @@ export async function listPiModels(): Promise<ModelInfo[]> {
|
||||
env: { ...process.env, PI_CODING_AGENT_DIR: PI_CONFIG_DIR },
|
||||
});
|
||||
|
||||
const output = proc.stdout.toString();
|
||||
const output = proc.stdout.toString() || proc.stderr.toString();
|
||||
|
||||
if (proc.exitCode !== 0) {
|
||||
const stderrText = proc.stderr.toString();
|
||||
|
||||
Reference in New Issue
Block a user