mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-11 03:13:12 +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:
1 parent
2ce5bb4ee3
commit
bd8daac094
3 files changed
+150
-98
No files matched your search
@@ -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
|
||||||
|
|||||||
@@ -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}`,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in new issue
Block a user