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:
kaitranntt
2025-12-21 22:26:11 -05:00
parent 6f8587db68
commit 2b441f6498
6 changed files with 90 additions and 21 deletions
+9 -1
View File
@@ -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 {
+22
View File
@@ -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'],
}; };
/** /**
+25 -16
View File
@@ -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
+9 -1
View File
@@ -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);
} }
+14 -2
View File
@@ -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) {
+11 -1
View File
@@ -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