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
2026-02-18 03:10:09 +07:00
parent 2ce5bb4ee3
commit bd8daac094
3 changed files with 150 additions and 98 deletions
+44 -56
View File
@@ -5,7 +5,27 @@
*/
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.
@@ -90,17 +110,17 @@ export function toKiroManagementMethod(method: KiroAuthMethod): 'aws' | 'google'
* - GHCP: 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>> = {
gemini: 8085,
codex: 1455,
agy: 51121,
iflow: 11451,
claude: 54545,
// kiro: Device Code Flow - no callback port
// qwen: Device Code Flow - no callback port
// ghcp: Device Code Flow - no callback port
// kimi: Device Code Flow - no callback port
};
export const OAUTH_CALLBACK_PORTS: Partial<Record<CLIProxyProvider, number>> =
CLIPROXY_PROVIDER_IDS.reduce(
(acc, provider) => {
const callbackPort = getOAuthCallbackPort(provider);
if (callbackPort !== null) {
acc[provider] = callbackPort;
}
return acc;
},
{} as Partial<Record<CLIProxyProvider, number>>
);
/**
* 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")
* Note: Gemini tokens may NOT have prefix - CLIProxyAPI uses {email}-{projectID}.json format
*/
export const PROVIDER_AUTH_PREFIXES: Record<CLIProxyProvider, string[]> = {
gemini: ['gemini-', 'google-'],
codex: ['codex-', 'openai-'],
agy: ['antigravity-', 'agy-'],
qwen: ['qwen-'],
iflow: ['iflow-'],
kiro: ['kiro-', 'aws-', 'codewhisperer-'],
ghcp: ['github-copilot-', 'copilot-', 'gh-'],
claude: ['claude-', 'anthropic-'],
kimi: ['kimi-'],
};
export const PROVIDER_AUTH_PREFIXES: Record<CLIProxyProvider, string[]> = buildProviderMap(
(provider) => [...getProviderAuthFilePrefixes(provider)]
);
/**
* Provider type values inside token JSON files
* CLIProxyAPI sets "type" field in token JSON (e.g., {"type": "gemini"})
*/
export const PROVIDER_TYPE_VALUES: Record<CLIProxyProvider, string[]> = {
gemini: ['gemini'],
codex: ['codex'],
agy: ['antigravity'],
qwen: ['qwen'],
iflow: ['iflow'],
kiro: ['kiro', 'codewhisperer'],
ghcp: ['github-copilot', 'copilot'],
claude: ['claude', 'anthropic'],
kimi: ['kimi'],
};
export const PROVIDER_TYPE_VALUES: Record<CLIProxyProvider, string[]> = buildProviderMap(
(provider) => [...getProviderTokenTypeValues(provider)]
);
/**
* Maps CCS provider names to CLIProxyAPI callback provider names
* Used when submitting OAuth callbacks to CLIProxyAPI management endpoint
*/
export const CLIPROXY_CALLBACK_PROVIDER_MAP: Record<CLIProxyProvider, string> = {
gemini: 'gemini',
codex: 'codex',
agy: 'antigravity',
kiro: 'kiro',
ghcp: 'copilot',
claude: 'anthropic',
qwen: 'qwen',
iflow: 'iflow',
kimi: 'kimi',
};
export const CLIPROXY_CALLBACK_PROVIDER_MAP: Record<CLIProxyProvider, string> = buildProviderMap(
(provider) => getCLIProxyCallbackProviderName(provider)
);
/**
* Maps CCS provider names to CLIProxyAPI auth-url endpoint prefixes.
* Used for GET /v0/management/${prefix}-auth-url endpoints.
* These differ from callback names for some providers (e.g., gemini-cli vs gemini).
*/
export const CLIPROXY_AUTH_URL_PROVIDER_MAP: Record<CLIProxyProvider, string> = {
gemini: 'gemini-cli',
codex: 'codex',
agy: 'antigravity',
kiro: 'kiro',
ghcp: 'github',
claude: 'anthropic',
qwen: 'qwen',
iflow: 'iflow',
kimi: 'kimi',
};
export const CLIPROXY_AUTH_URL_PROVIDER_MAP: Record<CLIProxyProvider, string> = buildProviderMap(
(provider) => getCLIProxyAuthUrlProviderName(provider)
);
/**
* Get OAuth config for provider
+26 -42
View File
@@ -11,6 +11,10 @@
*/
import { CLIProxyProvider } from '../../types';
import {
getTokenRefreshOwnership,
isRefreshDelegatedToCLIProxy,
} from '../../provider-capabilities';
import { refreshGeminiToken } from '../gemini-token-refresh';
/** Token refresh result */
@@ -22,26 +26,11 @@ export interface ProviderRefreshResult {
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
*/
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,
_accountId: string
): Promise<ProviderRefreshResult> {
switch (provider) {
case 'gemini':
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 (provider === 'gemini') {
return await refreshGeminiTokenWrapper();
}
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';
export type OAuthFlowType = 'authorization_code' | 'device_code';
export type TokenRefreshOwnership = 'ccs' | 'cliproxy' | 'unsupported';
export interface ProviderCapabilities {
displayName: string;
oauthFlow: OAuthFlowType;
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.
* These aliases normalize external names to canonical CCS provider IDs.
@@ -18,54 +29,99 @@ export const PROVIDER_CAPABILITIES: Record<CLIProxyProvider, ProviderCapabilitie
displayName: 'Google Gemini',
oauthFlow: 'authorization_code',
callbackPort: 8085,
callbackProviderName: 'gemini',
authUrlProviderName: 'gemini-cli',
refreshOwnership: 'ccs',
authFilePrefixes: ['gemini-', 'google-'],
tokenTypeValues: ['gemini'],
aliases: ['gemini-cli'],
},
codex: {
displayName: 'Codex',
oauthFlow: 'authorization_code',
callbackPort: 1455,
callbackProviderName: 'codex',
authUrlProviderName: 'codex',
refreshOwnership: 'cliproxy',
authFilePrefixes: ['codex-', 'openai-'],
tokenTypeValues: ['codex'],
aliases: [],
},
agy: {
displayName: 'AntiGravity',
oauthFlow: 'authorization_code',
callbackPort: 51121,
callbackProviderName: 'antigravity',
authUrlProviderName: 'antigravity',
refreshOwnership: 'cliproxy',
authFilePrefixes: ['antigravity-', 'agy-'],
tokenTypeValues: ['antigravity'],
aliases: ['antigravity'],
},
qwen: {
displayName: 'Qwen',
oauthFlow: 'device_code',
callbackPort: null,
callbackProviderName: 'qwen',
authUrlProviderName: 'qwen',
refreshOwnership: 'cliproxy',
authFilePrefixes: ['qwen-'],
tokenTypeValues: ['qwen'],
aliases: [],
},
iflow: {
displayName: 'iFlow',
oauthFlow: 'authorization_code',
callbackPort: 11451,
callbackProviderName: 'iflow',
authUrlProviderName: 'iflow',
refreshOwnership: 'cliproxy',
authFilePrefixes: ['iflow-'],
tokenTypeValues: ['iflow'],
aliases: [],
},
kiro: {
displayName: 'Kiro (AWS)',
oauthFlow: 'device_code',
callbackPort: null,
callbackProviderName: 'kiro',
authUrlProviderName: 'kiro',
refreshOwnership: 'cliproxy',
authFilePrefixes: ['kiro-', 'aws-', 'codewhisperer-'],
tokenTypeValues: ['kiro', 'codewhisperer'],
aliases: ['codewhisperer'],
},
ghcp: {
displayName: 'GitHub Copilot (OAuth)',
oauthFlow: 'device_code',
callbackPort: null,
callbackProviderName: 'copilot',
authUrlProviderName: 'github',
refreshOwnership: 'cliproxy',
authFilePrefixes: ['github-copilot-', 'copilot-', 'gh-'],
tokenTypeValues: ['github-copilot', 'copilot'],
aliases: ['github-copilot', 'copilot'],
},
claude: {
displayName: 'Claude',
oauthFlow: 'authorization_code',
callbackPort: 54545,
callbackProviderName: 'anthropic',
authUrlProviderName: 'anthropic',
refreshOwnership: 'unsupported',
authFilePrefixes: ['claude-', 'anthropic-'],
tokenTypeValues: ['claude', 'anthropic'],
aliases: ['anthropic'],
},
kimi: {
displayName: 'Kimi (Moonshot)',
oauthFlow: 'device_code',
callbackPort: null,
callbackProviderName: 'kimi',
authUrlProviderName: 'kimi',
refreshOwnership: 'cliproxy',
authFilePrefixes: ['kimi-'],
tokenTypeValues: ['kimi'],
aliases: ['moonshot'],
},
};
@@ -113,6 +169,30 @@ export function getOAuthCallbackPort(provider: CLIProxyProvider): number | null
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 {
const normalized = providerName.toLowerCase();
return PROVIDER_ALIAS_MAP.get(normalized) ?? null;