mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 02:11:28 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user