refactor(cliproxy): centralize provider auth capability metadata

- move callback/auth-url names, token type values, and prefixes

- derive auth maps from capabilities instead of hardcoded records

- route refresh delegation through capability ownership metadata
This commit is contained in:
Tam Nhu Tran committed 2026-02-18 03:10:09 +07:00
1 parent 2ce5bb4ee3
commit bd8daac094
3 files changed
+150 -98

No files matched your search

+44 -56
View File
@@ -5,7 +5,27 @@
*/ */
import { CLIProxyProvider } from '../types'; import { CLIProxyProvider } from '../types';
import { AccountInfo } from '../account-manager'; import type { AccountInfo } from '../account-manager';
import {
CLIPROXY_PROVIDER_IDS,
getOAuthCallbackPort,
getCLIProxyCallbackProviderName,
getCLIProxyAuthUrlProviderName,
getProviderAuthFilePrefixes,
getProviderTokenTypeValues,
} from '../provider-capabilities';
function buildProviderMap<T>(
valueFor: (provider: CLIProxyProvider) => T
): Record<CLIProxyProvider, T> {
return CLIPROXY_PROVIDER_IDS.reduce(
(acc, provider) => {
acc[provider] = valueFor(provider);
return acc;
},
{} as Record<CLIProxyProvider, T>
);
}
/** /**
* Kiro authentication methods supported by CLIProxyAPIPlus. * Kiro authentication methods supported by CLIProxyAPIPlus.
@@ -90,17 +110,17 @@ export function toKiroManagementMethod(method: KiroAuthMethod): 'aws' | 'google'
* - GHCP: Device Code Flow (polling-based, NO callback port needed) * - GHCP: Device Code Flow (polling-based, NO callback port needed)
* - Kimi: Device Code Flow (polling-based, NO callback port needed) * - Kimi: Device Code Flow (polling-based, NO callback port needed)
*/ */
export const OAUTH_CALLBACK_PORTS: Partial<Record<CLIProxyProvider, number>> = { export const OAUTH_CALLBACK_PORTS: Partial<Record<CLIProxyProvider, number>> =
gemini: 8085, CLIPROXY_PROVIDER_IDS.reduce(
codex: 1455, (acc, provider) => {
agy: 51121, const callbackPort = getOAuthCallbackPort(provider);
iflow: 11451, if (callbackPort !== null) {
claude: 54545, acc[provider] = callbackPort;
// kiro: Device Code Flow - no callback port }
// qwen: Device Code Flow - no callback port return acc;
// ghcp: Device Code Flow - no callback port },
// kimi: Device Code Flow - no callback port {} as Partial<Record<CLIProxyProvider, number>>
}; );
/** /**
* Auth status for a provider * Auth status for a provider
@@ -215,66 +235,34 @@ export const OAUTH_CONFIGS: Record<CLIProxyProvider, ProviderOAuthConfig> = {
* CLIProxyAPI names auth files with provider prefix (e.g., "antigravity-user@email.json") * CLIProxyAPI names auth files with provider prefix (e.g., "antigravity-user@email.json")
* Note: Gemini tokens may NOT have prefix - CLIProxyAPI uses {email}-{projectID}.json format * Note: Gemini tokens may NOT have prefix - CLIProxyAPI uses {email}-{projectID}.json format
*/ */
export const PROVIDER_AUTH_PREFIXES: Record<CLIProxyProvider, string[]> = { export const PROVIDER_AUTH_PREFIXES: Record<CLIProxyProvider, string[]> = buildProviderMap(
gemini: ['gemini-', 'google-'], (provider) => [...getProviderAuthFilePrefixes(provider)]
codex: ['codex-', 'openai-'], );
agy: ['antigravity-', 'agy-'],
qwen: ['qwen-'],
iflow: ['iflow-'],
kiro: ['kiro-', 'aws-', 'codewhisperer-'],
ghcp: ['github-copilot-', 'copilot-', 'gh-'],
claude: ['claude-', 'anthropic-'],
kimi: ['kimi-'],
};
/** /**
* Provider type values inside token JSON files * Provider type values inside token JSON files
* CLIProxyAPI sets "type" field in token JSON (e.g., {"type": "gemini"}) * CLIProxyAPI sets "type" field in token JSON (e.g., {"type": "gemini"})
*/ */
export const PROVIDER_TYPE_VALUES: Record<CLIProxyProvider, string[]> = { export const PROVIDER_TYPE_VALUES: Record<CLIProxyProvider, string[]> = buildProviderMap(
gemini: ['gemini'], (provider) => [...getProviderTokenTypeValues(provider)]
codex: ['codex'], );
agy: ['antigravity'],
qwen: ['qwen'],
iflow: ['iflow'],
kiro: ['kiro', 'codewhisperer'],
ghcp: ['github-copilot', 'copilot'],
claude: ['claude', 'anthropic'],
kimi: ['kimi'],
};
/** /**
* Maps CCS provider names to CLIProxyAPI callback provider names * Maps CCS provider names to CLIProxyAPI callback provider names
* Used when submitting OAuth callbacks to CLIProxyAPI management endpoint * Used when submitting OAuth callbacks to CLIProxyAPI management endpoint
*/ */
export const CLIPROXY_CALLBACK_PROVIDER_MAP: Record<CLIProxyProvider, string> = { export const CLIPROXY_CALLBACK_PROVIDER_MAP: Record<CLIProxyProvider, string> = buildProviderMap(
gemini: 'gemini', (provider) => getCLIProxyCallbackProviderName(provider)
codex: 'codex', );
agy: 'antigravity',
kiro: 'kiro',
ghcp: 'copilot',
claude: 'anthropic',
qwen: 'qwen',
iflow: 'iflow',
kimi: 'kimi',
};
/** /**
* Maps CCS provider names to CLIProxyAPI auth-url endpoint prefixes. * Maps CCS provider names to CLIProxyAPI auth-url endpoint prefixes.
* Used for GET /v0/management/${prefix}-auth-url endpoints. * Used for GET /v0/management/${prefix}-auth-url endpoints.
* These differ from callback names for some providers (e.g., gemini-cli vs gemini). * These differ from callback names for some providers (e.g., gemini-cli vs gemini).
*/ */
export const CLIPROXY_AUTH_URL_PROVIDER_MAP: Record<CLIProxyProvider, string> = { export const CLIPROXY_AUTH_URL_PROVIDER_MAP: Record<CLIProxyProvider, string> = buildProviderMap(
gemini: 'gemini-cli', (provider) => getCLIProxyAuthUrlProviderName(provider)
codex: 'codex', );
agy: 'antigravity',
kiro: 'kiro',
ghcp: 'github',
claude: 'anthropic',
qwen: 'qwen',
iflow: 'iflow',
kimi: 'kimi',
};
/** /**
* Get OAuth config for provider * Get OAuth config for provider
+26 -42
View File
@@ -11,6 +11,10 @@
*/ */
import { CLIProxyProvider } from '../../types'; import { CLIProxyProvider } from '../../types';
import {
getTokenRefreshOwnership,
isRefreshDelegatedToCLIProxy,
} from '../../provider-capabilities';
import { refreshGeminiToken } from '../gemini-token-refresh'; import { refreshGeminiToken } from '../gemini-token-refresh';
/** Token refresh result */ /** Token refresh result */
@@ -22,26 +26,11 @@ export interface ProviderRefreshResult {
delegated?: boolean; delegated?: boolean;
} }
/**
* Providers where CLIProxyAPIPlus owns token refresh.
* CLIProxyAPIPlus runs background refresh automatically (e.g. kiro: every 1 min).
* CCS should not attempt to refresh these — just trust CLIProxy.
*/
const CLIPROXY_DELEGATED_REFRESH: CLIProxyProvider[] = [
'codex',
'agy',
'kiro',
'ghcp',
'qwen',
'iflow',
'kimi',
];
/** /**
* Check if a provider's token refresh is delegated to CLIProxy * Check if a provider's token refresh is delegated to CLIProxy
*/ */
export function isRefreshDelegated(provider: CLIProxyProvider): boolean { export function isRefreshDelegated(provider: CLIProxyProvider): boolean {
return CLIPROXY_DELEGATED_REFRESH.includes(provider); return isRefreshDelegatedToCLIProxy(provider);
} }
/** /**
@@ -54,33 +43,28 @@ export async function refreshToken(
provider: CLIProxyProvider, provider: CLIProxyProvider,
_accountId: string _accountId: string
): Promise<ProviderRefreshResult> { ): Promise<ProviderRefreshResult> {
switch (provider) { if (provider === 'gemini') {
case 'gemini': return await refreshGeminiTokenWrapper();
return await refreshGeminiTokenWrapper();
case 'codex':
case 'agy':
case 'qwen':
case 'iflow':
case 'kiro':
case 'ghcp':
case 'kimi':
// CLIProxyAPIPlus handles refresh for these providers automatically.
// No action needed from CCS — report success with delegated flag.
return { success: true, delegated: true };
case 'claude':
return {
success: false,
error: `Token refresh not yet implemented for ${provider}`,
};
default:
return {
success: false,
error: `Unknown provider: ${provider}`,
};
} }
if (isRefreshDelegated(provider)) {
// CLIProxyAPIPlus handles refresh for these providers automatically.
// No action needed from CCS — report success with delegated flag.
return { success: true, delegated: true };
}
const ownership = getTokenRefreshOwnership(provider);
if (ownership === 'unsupported') {
return {
success: false,
error: `Token refresh not yet implemented for ${provider}`,
};
}
return {
success: false,
error: `Unknown provider: ${provider}`,
};
} }
/** /**
+80
View File
@@ -1,11 +1,22 @@
import type { CLIProxyProvider } from './types'; import type { CLIProxyProvider } from './types';
export type OAuthFlowType = 'authorization_code' | 'device_code'; export type OAuthFlowType = 'authorization_code' | 'device_code';
export type TokenRefreshOwnership = 'ccs' | 'cliproxy' | 'unsupported';
export interface ProviderCapabilities { export interface ProviderCapabilities {
displayName: string; displayName: string;
oauthFlow: OAuthFlowType; oauthFlow: OAuthFlowType;
callbackPort: number | null; callbackPort: number | null;
/** Provider name expected by CLIProxyAPI callback endpoint payload. */
callbackProviderName: string;
/** Provider name prefix used by CLIProxyAPI auth URL endpoint. */
authUrlProviderName: string;
/** Who owns token refresh logic for this provider. */
refreshOwnership: TokenRefreshOwnership;
/** Filename prefixes used to identify auth tokens for this provider. */
authFilePrefixes: readonly string[];
/** Token JSON "type" values accepted for this provider. */
tokenTypeValues: readonly string[];
/** /**
* Alternative provider names used by CLIProxyAPI or stats endpoints. * Alternative provider names used by CLIProxyAPI or stats endpoints.
* These aliases normalize external names to canonical CCS provider IDs. * These aliases normalize external names to canonical CCS provider IDs.
@@ -18,54 +29,99 @@ export const PROVIDER_CAPABILITIES: Record<CLIProxyProvider, ProviderCapabilitie
displayName: 'Google Gemini', displayName: 'Google Gemini',
oauthFlow: 'authorization_code', oauthFlow: 'authorization_code',
callbackPort: 8085, callbackPort: 8085,
callbackProviderName: 'gemini',
authUrlProviderName: 'gemini-cli',
refreshOwnership: 'ccs',
authFilePrefixes: ['gemini-', 'google-'],
tokenTypeValues: ['gemini'],
aliases: ['gemini-cli'], aliases: ['gemini-cli'],
}, },
codex: { codex: {
displayName: 'Codex', displayName: 'Codex',
oauthFlow: 'authorization_code', oauthFlow: 'authorization_code',
callbackPort: 1455, callbackPort: 1455,
callbackProviderName: 'codex',
authUrlProviderName: 'codex',
refreshOwnership: 'cliproxy',
authFilePrefixes: ['codex-', 'openai-'],
tokenTypeValues: ['codex'],
aliases: [], aliases: [],
}, },
agy: { agy: {
displayName: 'AntiGravity', displayName: 'AntiGravity',
oauthFlow: 'authorization_code', oauthFlow: 'authorization_code',
callbackPort: 51121, callbackPort: 51121,
callbackProviderName: 'antigravity',
authUrlProviderName: 'antigravity',
refreshOwnership: 'cliproxy',
authFilePrefixes: ['antigravity-', 'agy-'],
tokenTypeValues: ['antigravity'],
aliases: ['antigravity'], aliases: ['antigravity'],
}, },
qwen: { qwen: {
displayName: 'Qwen', displayName: 'Qwen',
oauthFlow: 'device_code', oauthFlow: 'device_code',
callbackPort: null, callbackPort: null,
callbackProviderName: 'qwen',
authUrlProviderName: 'qwen',
refreshOwnership: 'cliproxy',
authFilePrefixes: ['qwen-'],
tokenTypeValues: ['qwen'],
aliases: [], aliases: [],
}, },
iflow: { iflow: {
displayName: 'iFlow', displayName: 'iFlow',
oauthFlow: 'authorization_code', oauthFlow: 'authorization_code',
callbackPort: 11451, callbackPort: 11451,
callbackProviderName: 'iflow',
authUrlProviderName: 'iflow',
refreshOwnership: 'cliproxy',
authFilePrefixes: ['iflow-'],
tokenTypeValues: ['iflow'],
aliases: [], aliases: [],
}, },
kiro: { kiro: {
displayName: 'Kiro (AWS)', displayName: 'Kiro (AWS)',
oauthFlow: 'device_code', oauthFlow: 'device_code',
callbackPort: null, callbackPort: null,
callbackProviderName: 'kiro',
authUrlProviderName: 'kiro',
refreshOwnership: 'cliproxy',
authFilePrefixes: ['kiro-', 'aws-', 'codewhisperer-'],
tokenTypeValues: ['kiro', 'codewhisperer'],
aliases: ['codewhisperer'], aliases: ['codewhisperer'],
}, },
ghcp: { ghcp: {
displayName: 'GitHub Copilot (OAuth)', displayName: 'GitHub Copilot (OAuth)',
oauthFlow: 'device_code', oauthFlow: 'device_code',
callbackPort: null, callbackPort: null,
callbackProviderName: 'copilot',
authUrlProviderName: 'github',
refreshOwnership: 'cliproxy',
authFilePrefixes: ['github-copilot-', 'copilot-', 'gh-'],
tokenTypeValues: ['github-copilot', 'copilot'],
aliases: ['github-copilot', 'copilot'], aliases: ['github-copilot', 'copilot'],
}, },
claude: { claude: {
displayName: 'Claude', displayName: 'Claude',
oauthFlow: 'authorization_code', oauthFlow: 'authorization_code',
callbackPort: 54545, callbackPort: 54545,
callbackProviderName: 'anthropic',
authUrlProviderName: 'anthropic',
refreshOwnership: 'unsupported',
authFilePrefixes: ['claude-', 'anthropic-'],
tokenTypeValues: ['claude', 'anthropic'],
aliases: ['anthropic'], aliases: ['anthropic'],
}, },
kimi: { kimi: {
displayName: 'Kimi (Moonshot)', displayName: 'Kimi (Moonshot)',
oauthFlow: 'device_code', oauthFlow: 'device_code',
callbackPort: null, callbackPort: null,
callbackProviderName: 'kimi',
authUrlProviderName: 'kimi',
refreshOwnership: 'cliproxy',
authFilePrefixes: ['kimi-'],
tokenTypeValues: ['kimi'],
aliases: ['moonshot'], aliases: ['moonshot'],
}, },
}; };
@@ -113,6 +169,30 @@ export function getOAuthCallbackPort(provider: CLIProxyProvider): number | null
return PROVIDER_CAPABILITIES[provider].callbackPort; return PROVIDER_CAPABILITIES[provider].callbackPort;
} }
export function getCLIProxyCallbackProviderName(provider: CLIProxyProvider): string {
return PROVIDER_CAPABILITIES[provider].callbackProviderName;
}
export function getCLIProxyAuthUrlProviderName(provider: CLIProxyProvider): string {
return PROVIDER_CAPABILITIES[provider].authUrlProviderName;
}
export function getTokenRefreshOwnership(provider: CLIProxyProvider): TokenRefreshOwnership {
return PROVIDER_CAPABILITIES[provider].refreshOwnership;
}
export function isRefreshDelegatedToCLIProxy(provider: CLIProxyProvider): boolean {
return PROVIDER_CAPABILITIES[provider].refreshOwnership === 'cliproxy';
}
export function getProviderAuthFilePrefixes(provider: CLIProxyProvider): readonly string[] {
return PROVIDER_CAPABILITIES[provider].authFilePrefixes;
}
export function getProviderTokenTypeValues(provider: CLIProxyProvider): readonly string[] {
return PROVIDER_CAPABILITIES[provider].tokenTypeValues;
}
export function mapExternalProviderName(providerName: string): CLIProxyProvider | null { export function mapExternalProviderName(providerName: string): CLIProxyProvider | null {
const normalized = providerName.toLowerCase(); const normalized = providerName.toLowerCase();
return PROVIDER_ALIAS_MAP.get(normalized) ?? null; return PROVIDER_ALIAS_MAP.get(normalized) ?? null;