mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-10 02:17:11 +00:00
feat(auth): add Kiro and GitHub Copilot OAuth providers
- Add kiro (port 8329) and copilot (port 8330) to auth-types - Implement OAuth flows in oauth-handler - Update token-manager to include new providers - Add new providers to CLIPROXY_PROFILES - Update diagnostics and API routes for new providers
This commit is contained in:
@@ -20,7 +20,15 @@ import { loadUnifiedConfig, isUnifiedMode } from '../config/unified-config-loade
|
|||||||
export type ProfileType = 'settings' | 'account' | 'cliproxy' | 'copilot' | 'default';
|
export type ProfileType = 'settings' | 'account' | 'cliproxy' | 'copilot' | 'default';
|
||||||
|
|
||||||
/** CLIProxy profile names (OAuth-based, zero config) */
|
/** CLIProxy profile names (OAuth-based, zero config) */
|
||||||
export const CLIPROXY_PROFILES = ['gemini', 'codex', 'agy', 'qwen'] as const;
|
export const CLIPROXY_PROFILES = [
|
||||||
|
'gemini',
|
||||||
|
'codex',
|
||||||
|
'agy',
|
||||||
|
'qwen',
|
||||||
|
'iflow',
|
||||||
|
'kiro',
|
||||||
|
'copilot',
|
||||||
|
] as const;
|
||||||
export type CLIProxyProfileName = (typeof CLIPROXY_PROFILES)[number];
|
export type CLIProxyProfileName = (typeof CLIPROXY_PROFILES)[number];
|
||||||
|
|
||||||
export interface ProfileDetectionResult {
|
export interface ProfileDetectionResult {
|
||||||
|
|||||||
@@ -16,12 +16,16 @@ import { AccountInfo } from '../account-manager';
|
|||||||
* - Codex: Authorization Code Flow with local callback server on port 1455
|
* - Codex: Authorization Code Flow with local callback server on port 1455
|
||||||
* - Agy: Authorization Code Flow with local callback server on port 51121
|
* - Agy: Authorization Code Flow with local callback server on port 51121
|
||||||
* - Qwen: Device Code Flow (polling-based, NO callback port needed)
|
* - Qwen: Device Code Flow (polling-based, NO callback port needed)
|
||||||
|
* - Kiro: Authorization Code Flow with local callback server on port 9876
|
||||||
|
* - Copilot: 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,
|
gemini: 8085,
|
||||||
|
kiro: 9876,
|
||||||
// codex uses 1455
|
// codex uses 1455
|
||||||
// agy uses 51121
|
// agy uses 51121
|
||||||
// qwen uses Device Code Flow - no callback port needed
|
// qwen uses Device Code Flow - no callback port needed
|
||||||
|
// copilot uses Device Code Flow - no callback port needed
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -100,6 +104,20 @@ export const OAUTH_CONFIGS: Record<CLIProxyProvider, ProviderOAuthConfig> = {
|
|||||||
scopes: ['phone', 'profile', 'email'],
|
scopes: ['phone', 'profile', 'email'],
|
||||||
authFlag: '--iflow-login',
|
authFlag: '--iflow-login',
|
||||||
},
|
},
|
||||||
|
kiro: {
|
||||||
|
provider: 'kiro',
|
||||||
|
displayName: 'Kiro (AWS)',
|
||||||
|
authUrl: 'https://oidc.us-east-1.amazonaws.com',
|
||||||
|
scopes: ['codewhisperer:completions', 'codewhisperer:conversations'],
|
||||||
|
authFlag: '--kiro-login',
|
||||||
|
},
|
||||||
|
copilot: {
|
||||||
|
provider: 'copilot',
|
||||||
|
displayName: 'GitHub Copilot',
|
||||||
|
authUrl: 'https://github.com/login/device/code',
|
||||||
|
scopes: ['copilot'],
|
||||||
|
authFlag: '--github-copilot-login',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -113,6 +131,8 @@ export const PROVIDER_AUTH_PREFIXES: Record<CLIProxyProvider, string[]> = {
|
|||||||
agy: ['antigravity-', 'agy-'],
|
agy: ['antigravity-', 'agy-'],
|
||||||
qwen: ['qwen-'],
|
qwen: ['qwen-'],
|
||||||
iflow: ['iflow-'],
|
iflow: ['iflow-'],
|
||||||
|
kiro: ['kiro-', 'aws-', 'codewhisperer-'],
|
||||||
|
copilot: ['github-copilot-', 'copilot-', 'gh-'],
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -125,6 +145,8 @@ export const PROVIDER_TYPE_VALUES: Record<CLIProxyProvider, string[]> = {
|
|||||||
agy: ['antigravity'],
|
agy: ['antigravity'],
|
||||||
qwen: ['qwen'],
|
qwen: ['qwen'],
|
||||||
iflow: ['iflow'],
|
iflow: ['iflow'],
|
||||||
|
kiro: ['kiro', 'codewhisperer'],
|
||||||
|
copilot: ['github-copilot', 'copilot'],
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* OAuth Handler for CLIProxyAPI
|
* OAuth Handler for CLIProxyAPI
|
||||||
*
|
*
|
||||||
* Manages OAuth authentication flow for CLIProxy providers (Gemini, Codex, Antigravity).
|
* Manages OAuth authentication flow for CLIProxy providers (Gemini, Codex, Antigravity, Kiro, Copilot).
|
||||||
* CLIProxyAPI handles OAuth internally - we just need to:
|
* CLIProxyAPI handles OAuth internally - we just need to:
|
||||||
* 1. Check if auth exists (token files in CCS auth directory)
|
* 1. Check if auth exists (token files in CCS auth directory)
|
||||||
* 2. Trigger OAuth flow by spawning binary with auth flag
|
* 2. Trigger OAuth flow by spawning binary with auth flag
|
||||||
* 3. Auto-detect headless environments (SSH, no DISPLAY)
|
* 3. Auto-detect headless environments (SSH, no DISPLAY)
|
||||||
* 4. Use --no-browser flag for headless, display OAuth URL for manual auth
|
* 4. Use --no-browser flag for headless, display OAuth URL for manual auth
|
||||||
|
* 5. Handle Device Code flows for Copilot/Qwen (no callback server)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
@@ -118,6 +119,7 @@ async function prepareBinary(
|
|||||||
* Trigger OAuth flow for provider
|
* Trigger OAuth flow for provider
|
||||||
* Auto-detects headless environment and uses --no-browser flag accordingly
|
* Auto-detects headless environment and uses --no-browser flag accordingly
|
||||||
* Shows real-time step-by-step progress for better user feedback
|
* Shows real-time step-by-step progress for better user feedback
|
||||||
|
* Handles both Authorization Code (callback server) and Device Code (polling) flows
|
||||||
*/
|
*/
|
||||||
export async function triggerOAuth(
|
export async function triggerOAuth(
|
||||||
provider: CLIProxyProvider,
|
provider: CLIProxyProvider,
|
||||||
@@ -128,6 +130,7 @@ export async function triggerOAuth(
|
|||||||
const callbackPort = OAUTH_PORTS[provider];
|
const callbackPort = OAUTH_PORTS[provider];
|
||||||
const isCLI = !fromUI;
|
const isCLI = !fromUI;
|
||||||
const headless = options.headless ?? isHeadlessEnvironment();
|
const headless = options.headless ?? isHeadlessEnvironment();
|
||||||
|
const isDeviceCodeFlow = callbackPort === null;
|
||||||
|
|
||||||
// Check for existing accounts
|
// Check for existing accounts
|
||||||
const existingAccounts = getProviderAccounts(provider);
|
const existingAccounts = getProviderAccounts(provider);
|
||||||
@@ -145,8 +148,8 @@ export async function triggerOAuth(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre-flight checks
|
// Pre-flight checks (skip for device code flows which don't need callback ports)
|
||||||
if (!(await runPreflightChecks(provider, oauthConfig))) {
|
if (!isDeviceCodeFlow && !(await runPreflightChecks(provider, oauthConfig))) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +161,7 @@ export async function triggerOAuth(
|
|||||||
|
|
||||||
const { binaryPath, tokenDir, configPath } = prepared;
|
const { binaryPath, tokenDir, configPath } = prepared;
|
||||||
|
|
||||||
// Free callback port if needed
|
// Free callback port if needed (only for authorization code flows)
|
||||||
const localCallbackPort = OAUTH_CALLBACK_PORTS[provider];
|
const localCallbackPort = OAUTH_CALLBACK_PORTS[provider];
|
||||||
if (localCallbackPort) {
|
if (localCallbackPort) {
|
||||||
const killed = killProcessOnPort(localCallbackPort, verbose);
|
const killed = killProcessOnPort(localCallbackPort, verbose);
|
||||||
@@ -173,19 +176,25 @@ export async function triggerOAuth(
|
|||||||
args.push('--no-browser');
|
args.push('--no-browser');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show callback server step
|
// Show step based on flow type
|
||||||
showStep(2, 4, 'progress', `Starting callback server on port ${callbackPort || 'N/A'}...`);
|
if (isDeviceCodeFlow) {
|
||||||
|
showStep(2, 4, 'progress', `Starting ${oauthConfig.displayName} Device Code flow...`);
|
||||||
|
console.log('');
|
||||||
|
console.log(info('Device Code Flow - follow the instructions below'));
|
||||||
|
} else {
|
||||||
|
showStep(2, 4, 'progress', `Starting callback server on port ${callbackPort}...`);
|
||||||
|
|
||||||
// Show headless instructions
|
// Show headless instructions (only for authorization code flows)
|
||||||
if (headless) {
|
if (headless) {
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(warn('PORT FORWARDING REQUIRED'));
|
console.log(warn('PORT FORWARDING REQUIRED'));
|
||||||
console.log(` OAuth callback uses localhost:${callbackPort} which must be reachable.`);
|
console.log(` OAuth callback uses localhost:${callbackPort} which must be reachable.`);
|
||||||
console.log(' Run this on your LOCAL machine:');
|
console.log(' Run this on your LOCAL machine:');
|
||||||
console.log(
|
console.log(
|
||||||
` ${color(`ssh -L ${callbackPort}:localhost:${callbackPort} <USER>@<HOST>`, 'command')}`
|
` ${color(`ssh -L ${callbackPort}:localhost:${callbackPort} <USER>@<HOST>`, 'command')}`
|
||||||
);
|
);
|
||||||
console.log('');
|
console.log('');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute OAuth process
|
// Execute OAuth process
|
||||||
|
|||||||
@@ -145,7 +145,15 @@ export function getAuthStatus(provider: CLIProxyProvider): AuthStatus {
|
|||||||
* Get auth status for all providers
|
* Get auth status for all providers
|
||||||
*/
|
*/
|
||||||
export function getAllAuthStatus(): AuthStatus[] {
|
export function getAllAuthStatus(): AuthStatus[] {
|
||||||
const providers: CLIProxyProvider[] = ['gemini', 'codex', 'agy', 'qwen', 'iflow'];
|
const providers: CLIProxyProvider[] = [
|
||||||
|
'gemini',
|
||||||
|
'codex',
|
||||||
|
'agy',
|
||||||
|
'qwen',
|
||||||
|
'iflow',
|
||||||
|
'kiro',
|
||||||
|
'copilot',
|
||||||
|
];
|
||||||
return providers.map(getAuthStatus);
|
return providers.map(getAuthStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ export const OAUTH_CALLBACK_PORTS: Record<CLIProxyProvider, number | null> = {
|
|||||||
agy: 51121,
|
agy: 51121,
|
||||||
qwen: null, // Device Code Flow - no callback port
|
qwen: null, // Device Code Flow - no callback port
|
||||||
iflow: null, // Device Code Flow - no callback port
|
iflow: null, // Device Code Flow - no callback port
|
||||||
|
kiro: 9876, // Authorization Code Flow
|
||||||
|
copilot: null, // Device Code Flow - no callback port
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,6 +50,8 @@ export const OAUTH_FLOW_TYPES: Record<CLIProxyProvider, OAuthFlowType> = {
|
|||||||
agy: 'authorization_code',
|
agy: 'authorization_code',
|
||||||
qwen: 'device_code',
|
qwen: 'device_code',
|
||||||
iflow: 'device_code',
|
iflow: 'device_code',
|
||||||
|
kiro: 'authorization_code',
|
||||||
|
copilot: 'device_code',
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -134,7 +138,15 @@ export async function checkOAuthPort(provider: CLIProxyProvider): Promise<OAuthP
|
|||||||
* Check OAuth ports for all providers
|
* Check OAuth ports for all providers
|
||||||
*/
|
*/
|
||||||
export async function checkAllOAuthPorts(): Promise<OAuthPortDiagnostic[]> {
|
export async function checkAllOAuthPorts(): Promise<OAuthPortDiagnostic[]> {
|
||||||
const providers: CLIProxyProvider[] = ['gemini', 'codex', 'agy', 'qwen', 'iflow'];
|
const providers: CLIProxyProvider[] = [
|
||||||
|
'gemini',
|
||||||
|
'codex',
|
||||||
|
'agy',
|
||||||
|
'qwen',
|
||||||
|
'iflow',
|
||||||
|
'kiro',
|
||||||
|
'copilot',
|
||||||
|
];
|
||||||
const results: OAuthPortDiagnostic[] = [];
|
const results: OAuthPortDiagnostic[] = [];
|
||||||
|
|
||||||
for (const provider of providers) {
|
for (const provider of providers) {
|
||||||
@@ -149,7 +161,7 @@ export async function checkAllOAuthPorts(): Promise<OAuthPortDiagnostic[]> {
|
|||||||
* Check OAuth ports for providers that use Authorization Code flow only
|
* Check OAuth ports for providers that use Authorization Code flow only
|
||||||
*/
|
*/
|
||||||
export async function checkAuthCodePorts(): Promise<OAuthPortDiagnostic[]> {
|
export async function checkAuthCodePorts(): Promise<OAuthPortDiagnostic[]> {
|
||||||
const providers: CLIProxyProvider[] = ['gemini', 'codex', 'agy'];
|
const providers: CLIProxyProvider[] = ['gemini', 'codex', 'agy', 'kiro'];
|
||||||
const results: OAuthPortDiagnostic[] = [];
|
const results: OAuthPortDiagnostic[] = [];
|
||||||
|
|
||||||
for (const provider of providers) {
|
for (const provider of providers) {
|
||||||
|
|||||||
@@ -28,7 +28,15 @@ import type { CLIProxyProvider } from '../../cliproxy/types';
|
|||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
// Valid providers list
|
// Valid providers list
|
||||||
const validProviders: CLIProxyProvider[] = ['gemini', 'codex', 'agy', 'qwen', 'iflow'];
|
const validProviders: CLIProxyProvider[] = [
|
||||||
|
'gemini',
|
||||||
|
'codex',
|
||||||
|
'agy',
|
||||||
|
'qwen',
|
||||||
|
'iflow',
|
||||||
|
'kiro',
|
||||||
|
'copilot',
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/cliproxy/auth - Get auth status for built-in CLIProxy profiles
|
* GET /api/cliproxy/auth - Get auth status for built-in CLIProxy profiles
|
||||||
@@ -57,6 +65,8 @@ router.get('/', async (_req: Request, res: Response): Promise<void> => {
|
|||||||
codex: 'codex',
|
codex: 'codex',
|
||||||
qwen: 'qwen',
|
qwen: 'qwen',
|
||||||
iflow: 'iflow',
|
iflow: 'iflow',
|
||||||
|
kiro: 'kiro',
|
||||||
|
copilot: 'copilot',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update lastUsedAt for providers with recent activity
|
// Update lastUsedAt for providers with recent activity
|
||||||
|
|||||||
Reference in New Issue
Block a user