From 2b441f64982c74174cb350537956e24970ef69f4 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Sun, 21 Dec 2025 22:26:11 -0500 Subject: [PATCH] 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 --- src/auth/profile-detector.ts | 10 ++++- src/cliproxy/auth/auth-types.ts | 22 ++++++++++ src/cliproxy/auth/oauth-handler.ts | 41 +++++++++++-------- src/cliproxy/auth/token-manager.ts | 10 ++++- src/management/oauth-port-diagnostics.ts | 16 +++++++- src/web-server/routes/cliproxy-auth-routes.ts | 12 +++++- 6 files changed, 90 insertions(+), 21 deletions(-) diff --git a/src/auth/profile-detector.ts b/src/auth/profile-detector.ts index 72955947..7016a0ae 100644 --- a/src/auth/profile-detector.ts +++ b/src/auth/profile-detector.ts @@ -20,7 +20,15 @@ import { loadUnifiedConfig, isUnifiedMode } from '../config/unified-config-loade export type ProfileType = 'settings' | 'account' | 'cliproxy' | 'copilot' | 'default'; /** 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 interface ProfileDetectionResult { diff --git a/src/cliproxy/auth/auth-types.ts b/src/cliproxy/auth/auth-types.ts index ddabae1c..3fc7336f 100644 --- a/src/cliproxy/auth/auth-types.ts +++ b/src/cliproxy/auth/auth-types.ts @@ -16,12 +16,16 @@ import { AccountInfo } from '../account-manager'; * - Codex: Authorization Code Flow with local callback server on port 1455 * - Agy: Authorization Code Flow with local callback server on port 51121 * - 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> = { gemini: 8085, + kiro: 9876, // codex uses 1455 // agy uses 51121 // 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 = { scopes: ['phone', 'profile', 'email'], 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 = { agy: ['antigravity-', 'agy-'], qwen: ['qwen-'], iflow: ['iflow-'], + kiro: ['kiro-', 'aws-', 'codewhisperer-'], + copilot: ['github-copilot-', 'copilot-', 'gh-'], }; /** @@ -125,6 +145,8 @@ export const PROVIDER_TYPE_VALUES: Record = { agy: ['antigravity'], qwen: ['qwen'], iflow: ['iflow'], + kiro: ['kiro', 'codewhisperer'], + copilot: ['github-copilot', 'copilot'], }; /** diff --git a/src/cliproxy/auth/oauth-handler.ts b/src/cliproxy/auth/oauth-handler.ts index b27201d6..f2bb51c2 100644 --- a/src/cliproxy/auth/oauth-handler.ts +++ b/src/cliproxy/auth/oauth-handler.ts @@ -1,12 +1,13 @@ /** * 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: * 1. Check if auth exists (token files in CCS auth directory) * 2. Trigger OAuth flow by spawning binary with auth flag * 3. Auto-detect headless environments (SSH, no DISPLAY) * 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'; @@ -118,6 +119,7 @@ async function prepareBinary( * Trigger OAuth flow for provider * Auto-detects headless environment and uses --no-browser flag accordingly * 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( provider: CLIProxyProvider, @@ -128,6 +130,7 @@ export async function triggerOAuth( const callbackPort = OAUTH_PORTS[provider]; const isCLI = !fromUI; const headless = options.headless ?? isHeadlessEnvironment(); + const isDeviceCodeFlow = callbackPort === null; // Check for existing accounts const existingAccounts = getProviderAccounts(provider); @@ -145,8 +148,8 @@ export async function triggerOAuth( } } - // Pre-flight checks - if (!(await runPreflightChecks(provider, oauthConfig))) { + // Pre-flight checks (skip for device code flows which don't need callback ports) + if (!isDeviceCodeFlow && !(await runPreflightChecks(provider, oauthConfig))) { return null; } @@ -158,7 +161,7 @@ export async function triggerOAuth( 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]; if (localCallbackPort) { const killed = killProcessOnPort(localCallbackPort, verbose); @@ -173,19 +176,25 @@ export async function triggerOAuth( args.push('--no-browser'); } - // Show callback server step - showStep(2, 4, 'progress', `Starting callback server on port ${callbackPort || 'N/A'}...`); + // Show step based on flow type + 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 - if (headless) { - console.log(''); - console.log(warn('PORT FORWARDING REQUIRED')); - console.log(` OAuth callback uses localhost:${callbackPort} which must be reachable.`); - console.log(' Run this on your LOCAL machine:'); - console.log( - ` ${color(`ssh -L ${callbackPort}:localhost:${callbackPort} @`, 'command')}` - ); - console.log(''); + // Show headless instructions (only for authorization code flows) + if (headless) { + console.log(''); + console.log(warn('PORT FORWARDING REQUIRED')); + console.log(` OAuth callback uses localhost:${callbackPort} which must be reachable.`); + console.log(' Run this on your LOCAL machine:'); + console.log( + ` ${color(`ssh -L ${callbackPort}:localhost:${callbackPort} @`, 'command')}` + ); + console.log(''); + } } // Execute OAuth process diff --git a/src/cliproxy/auth/token-manager.ts b/src/cliproxy/auth/token-manager.ts index c1e4fafe..e16ea424 100644 --- a/src/cliproxy/auth/token-manager.ts +++ b/src/cliproxy/auth/token-manager.ts @@ -145,7 +145,15 @@ export function getAuthStatus(provider: CLIProxyProvider): AuthStatus { * Get auth status for all providers */ 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); } diff --git a/src/management/oauth-port-diagnostics.ts b/src/management/oauth-port-diagnostics.ts index 84828f8d..f789ce3a 100644 --- a/src/management/oauth-port-diagnostics.ts +++ b/src/management/oauth-port-diagnostics.ts @@ -32,6 +32,8 @@ export const OAUTH_CALLBACK_PORTS: Record = { agy: 51121, qwen: 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 = { agy: 'authorization_code', qwen: 'device_code', iflow: 'device_code', + kiro: 'authorization_code', + copilot: 'device_code', }; /** @@ -134,7 +138,15 @@ export async function checkOAuthPort(provider: CLIProxyProvider): Promise { - const providers: CLIProxyProvider[] = ['gemini', 'codex', 'agy', 'qwen', 'iflow']; + const providers: CLIProxyProvider[] = [ + 'gemini', + 'codex', + 'agy', + 'qwen', + 'iflow', + 'kiro', + 'copilot', + ]; const results: OAuthPortDiagnostic[] = []; for (const provider of providers) { @@ -149,7 +161,7 @@ export async function checkAllOAuthPorts(): Promise { * Check OAuth ports for providers that use Authorization Code flow only */ export async function checkAuthCodePorts(): Promise { - const providers: CLIProxyProvider[] = ['gemini', 'codex', 'agy']; + const providers: CLIProxyProvider[] = ['gemini', 'codex', 'agy', 'kiro']; const results: OAuthPortDiagnostic[] = []; for (const provider of providers) { diff --git a/src/web-server/routes/cliproxy-auth-routes.ts b/src/web-server/routes/cliproxy-auth-routes.ts index feeff110..1b3ed88b 100644 --- a/src/web-server/routes/cliproxy-auth-routes.ts +++ b/src/web-server/routes/cliproxy-auth-routes.ts @@ -28,7 +28,15 @@ import type { CLIProxyProvider } from '../../cliproxy/types'; const router = Router(); // 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 @@ -57,6 +65,8 @@ router.get('/', async (_req: Request, res: Response): Promise => { codex: 'codex', qwen: 'qwen', iflow: 'iflow', + kiro: 'kiro', + copilot: 'copilot', }; // Update lastUsedAt for providers with recent activity