fix(cliproxy): diagnose Gemini Plus OAuth credentials

Closes #1131
This commit is contained in:
Kai (Tam Nhu) Tran
2026-05-07 11:05:53 -04:00
committed by GitHub
parent 5e06010a4e
commit 61390e5691
2 changed files with 127 additions and 2 deletions
@@ -80,6 +80,69 @@ describe('requestPasteCallbackStart', () => {
});
});
describe('Gemini Plus OAuth credential diagnostics', () => {
it('fails fast when Gemini uses Plus without OAuth client env', async () => {
const { getGeminiPlusOAuthCredentialError } = await import(
`../oauth-handler?gemini-plus-missing-env=${Date.now()}`
);
const error = getGeminiPlusOAuthCredentialError('gemini', 'plus', {});
expect(error).toContain('Gemini OAuth from CLIProxy Plus is missing');
expect(error).toContain('CLIPROXY_GEMINI_OAUTH_CLIENT_ID');
expect(error).toContain('CLIPROXY_GEMINI_OAUTH_CLIENT_SECRET');
expect(error).toContain('cliproxy.backend');
expect(error).toContain('original');
});
it('allows Gemini Plus when both OAuth client env values exist', async () => {
const { getGeminiPlusOAuthCredentialError } = await import(
`../oauth-handler?gemini-plus-env-present=${Date.now()}`
);
expect(
getGeminiPlusOAuthCredentialError('gemini', 'plus', {
CLIPROXY_GEMINI_OAUTH_CLIENT_ID: 'client-id',
CLIPROXY_GEMINI_OAUTH_CLIENT_SECRET: 'client-secret',
})
).toBeNull();
});
it('does not warn for Gemini on the original backend', async () => {
const { getGeminiPlusOAuthCredentialError } = await import(
`../oauth-handler?gemini-original-backend=${Date.now()}`
);
expect(getGeminiPlusOAuthCredentialError('gemini', 'original', {})).toBeNull();
});
it('detects Gemini auth URLs missing client_id before display', async () => {
const { getGeminiAuthUrlCredentialError } = await import(
`../oauth-handler?gemini-auth-url-missing-client=${Date.now()}`
);
const error = getGeminiAuthUrlCredentialError(
'gemini',
'https://accounts.google.com/o/oauth2/v2/auth?client_id=&redirect_uri=http%3A%2F%2Flocalhost%3A8085%2Foauth2callback&state=test'
);
expect(error).toContain('Gemini OAuth from CLIProxy Plus is missing');
});
it('allows Gemini auth URLs with client_id present', async () => {
const { getGeminiAuthUrlCredentialError } = await import(
`../oauth-handler?gemini-auth-url-client-present=${Date.now()}`
);
expect(
getGeminiAuthUrlCredentialError(
'gemini',
'https://accounts.google.com/o/oauth2/v2/auth?client_id=test-client&redirect_uri=http%3A%2F%2Flocalhost%3A8085%2Foauth2callback&state=test'
)
).toBeNull();
});
});
describe('usesKiroLocalCallbackReplay', () => {
it('limits local callback replay to CLI auth-code flows', async () => {
const { usesKiroLocalCallbackReplay } = await import(
+64 -2
View File
@@ -13,9 +13,9 @@
import * as fs from 'fs';
import { fail, info, warn, color, ok } from '../../utils/ui';
import { createLogger } from '../../services/logging';
import { ensureCLIProxyBinary } from '../binary-manager';
import { ensureCLIProxyBinary, getStoredConfiguredBackend } from '../binary-manager';
import { generateConfig } from '../config/config-generator';
import { CLIProxyProvider } from '../types';
import { CLIProxyBackend, CLIProxyProvider } from '../types';
import {
AccountInfo,
getProviderAccounts,
@@ -82,9 +82,54 @@ interface PasteCallbackStartData {
const PASTE_CALLBACK_AUTH_URL_POLL_INTERVAL_MS = 3000;
const POLLED_AUTH_LOCAL_TOKEN_GRACE_MS = 15 * 1000;
const GEMINI_PLUS_CLIENT_ID_ENV = 'CLIPROXY_GEMINI_OAUTH_CLIENT_ID';
const GEMINI_PLUS_CLIENT_SECRET_ENV = 'CLIPROXY_GEMINI_OAUTH_CLIENT_SECRET';
const logger = createLogger('cliproxy:auth:oauth');
function buildGeminiPlusOAuthCredentialMessage(missing?: string[]): string {
const missingText = missing?.length ? ` Missing: ${missing.join(', ')}.` : '';
return (
'Gemini OAuth from CLIProxy Plus is missing Google OAuth client credentials.' +
missingText +
` Set ${GEMINI_PLUS_CLIENT_ID_ENV} and ${GEMINI_PLUS_CLIENT_SECRET_ENV} before starting CLIProxy Plus,` +
' or switch `cliproxy.backend` to `original` for Gemini.'
);
}
export function getGeminiPlusOAuthCredentialError(
provider: CLIProxyProvider,
backend: CLIProxyBackend,
env: NodeJS.ProcessEnv = process.env
): string | null {
if (provider !== 'gemini' || backend !== 'plus') {
return null;
}
const missing = [GEMINI_PLUS_CLIENT_ID_ENV, GEMINI_PLUS_CLIENT_SECRET_ENV].filter(
(name) => !env[name]?.trim()
);
return missing.length > 0 ? buildGeminiPlusOAuthCredentialMessage(missing) : null;
}
export function getGeminiAuthUrlCredentialError(
provider: CLIProxyProvider,
authUrl: string
): string | null {
if (provider !== 'gemini') {
return null;
}
try {
const parsed = new URL(authUrl);
const clientId = parsed.searchParams.get('client_id')?.trim();
return clientId ? null : buildGeminiPlusOAuthCredentialMessage();
} catch {
return null;
}
}
export async function requestPasteCallbackStart(
provider: CLIProxyProvider,
target: ProxyTarget,
@@ -549,6 +594,12 @@ async function handlePasteCallbackMode(
return null;
}
const authUrlCredentialError = getGeminiAuthUrlCredentialError(provider, authUrl);
if (authUrlCredentialError) {
console.log(fail(authUrlCredentialError));
return null;
}
const oauthState = startData.state || parseAuthUrlState(authUrl);
const knownTokenFiles = listProviderTokenSnapshots(provider, tokenDir);
@@ -920,6 +971,17 @@ export async function triggerOAuth(
const useSelectedKiroDirectCliFlow =
provider === 'kiro' && (isDeviceCodeFlow || useSelectedKiroLocalPasteCallback);
if (!(selectedPasteCallback && !useSelectedKiroDirectCliFlow)) {
const credentialError = getGeminiPlusOAuthCredentialError(
provider,
getStoredConfiguredBackend()
);
if (credentialError) {
console.log(fail(credentialError));
return null;
}
}
if (existingAccounts.length > 0 && !add) {
console.log('');
console.log(