diff --git a/src/cliproxy/auth/auth-types.ts b/src/cliproxy/auth/auth-types.ts index 1f66114f..91feaa70 100644 --- a/src/cliproxy/auth/auth-types.ts +++ b/src/cliproxy/auth/auth-types.ts @@ -7,6 +7,74 @@ import { CLIProxyProvider } from '../types'; import { AccountInfo } from '../account-manager'; +/** + * Kiro authentication methods supported by CLIProxyAPIPlus. + * - aws: AWS Builder ID via Device Code flow + * - aws-authcode: AWS Builder ID via Authorization Code flow (CLI flag only) + * - google: Social OAuth via Google + * - github: Social OAuth via GitHub (management API only) + */ +export const KIRO_AUTH_METHODS = ['aws', 'aws-authcode', 'google', 'github'] as const; +export type KiroAuthMethod = (typeof KIRO_AUTH_METHODS)[number]; + +/** CLI binary supports these Kiro methods directly via flags. */ +export const KIRO_CLI_AUTH_METHODS = ['aws', 'aws-authcode', 'google'] as const; +export type KiroCLIAuthMethod = (typeof KIRO_CLI_AUTH_METHODS)[number]; + +/** Default Kiro method for CCS UX and AWS Organization support. */ +export const DEFAULT_KIRO_AUTH_METHOD: KiroAuthMethod = 'aws'; + +export function isKiroAuthMethod(value: string): value is KiroAuthMethod { + return KIRO_AUTH_METHODS.includes(value as KiroAuthMethod); +} + +export function isKiroCLIAuthMethod(value: string): value is KiroCLIAuthMethod { + return KIRO_CLI_AUTH_METHODS.includes(value as KiroCLIAuthMethod); +} + +export function normalizeKiroAuthMethod(value?: string): KiroAuthMethod { + if (!value) return DEFAULT_KIRO_AUTH_METHOD; + const normalized = value.trim().toLowerCase(); + return isKiroAuthMethod(normalized) ? normalized : DEFAULT_KIRO_AUTH_METHOD; +} + +export function isKiroDeviceCodeMethod(method: KiroAuthMethod): boolean { + return method === 'aws'; +} + +export function getKiroCallbackPort(method: KiroAuthMethod): number | null { + return isKiroDeviceCodeMethod(method) ? null : 9876; +} + +export function getKiroCLIAuthFlag(method: KiroCLIAuthMethod): string { + switch (method) { + case 'aws': + return '--kiro-aws-login'; + case 'aws-authcode': + return '--kiro-aws-authcode'; + case 'google': + return '--kiro-google-login'; + } +} + +/** + * Kiro method for CLIProxyAPI management endpoint: + * GET /v0/management/kiro-auth-url?method= + */ +export function toKiroManagementMethod(method: KiroAuthMethod): 'aws' | 'google' | 'github' { + switch (method) { + case 'google': + return 'google'; + case 'github': + return 'github'; + case 'aws-authcode': + return 'aws'; + case 'aws': + default: + return 'aws'; + } +} + /** * OAuth callback ports used by CLIProxyAPI (hardcoded in binary) * See: https://github.com/router-for-me/CLIProxyAPI/tree/main/internal/auth @@ -113,7 +181,9 @@ export const OAUTH_CONFIGS: Record = { displayName: 'Kiro (AWS)', authUrl: 'https://oidc.us-east-1.amazonaws.com', scopes: ['codewhisperer:completions', 'codewhisperer:conversations'], - authFlag: '--kiro-login', + // Default to AWS Builder ID device code flow for better compatibility. + // Other Kiro methods are selected at runtime via OAuthOptions.kiroMethod. + authFlag: '--kiro-aws-login', }, ghcp: { provider: 'ghcp', @@ -213,6 +283,8 @@ export interface OAuthOptions { account?: string; add?: boolean; nickname?: string; + /** Kiro auth method override (CLI + Dashboard parity). */ + kiroMethod?: KiroAuthMethod; /** If true, triggered from Web UI (enables project selection prompt) */ fromUI?: boolean; /** If true, use --no-incognito flag (Kiro only - use normal browser instead of incognito) */ diff --git a/src/cliproxy/auth/oauth-handler.ts b/src/cliproxy/auth/oauth-handler.ts index 29e47e98..dc651216 100644 --- a/src/cliproxy/auth/oauth-handler.ts +++ b/src/cliproxy/auth/oauth-handler.ts @@ -29,10 +29,15 @@ import { } from '../../management/oauth-port-diagnostics'; import { OAuthOptions, - OAUTH_CALLBACK_PORTS, + DEFAULT_KIRO_AUTH_METHOD, + getKiroCallbackPort, + getKiroCLIAuthFlag, + isKiroCLIAuthMethod, + isKiroDeviceCodeMethod, getOAuthConfig, ProviderOAuthConfig, CLIPROXY_CALLBACK_PROVIDER_MAP, + normalizeKiroAuthMethod, } from './auth-types'; import { isHeadlessEnvironment, killProcessOnPort, showStep } from './environment-detector'; import { getProviderTokenDir, isAuthenticated, registerAccountFromToken } from './token-manager'; @@ -414,6 +419,8 @@ export async function triggerOAuth( const oauthConfig = getOAuthConfig(provider); const { verbose = false, add = false, fromUI = false, noIncognito = true } = options; let { nickname } = options; + const resolvedKiroMethod = + provider === 'kiro' ? normalizeKiroAuthMethod(options.kiroMethod) : DEFAULT_KIRO_AUTH_METHOD; // Check for existing accounts const existingAccounts = getProviderAccounts(provider); @@ -444,10 +451,28 @@ export async function triggerOAuth( return null; } - const callbackPort = OAUTH_PORTS[provider]; + if (provider === 'kiro' && resolvedKiroMethod === 'github') { + console.log(fail('Kiro GitHub login is only available in Dashboard management OAuth flow.')); + console.log(' Use: ccs config -> Accounts -> Add Kiro account -> Method: GitHub OAuth'); + return null; + } + + const callbackPort = + provider === 'kiro' ? getKiroCallbackPort(resolvedKiroMethod) : OAUTH_PORTS[provider]; const isCLI = !fromUI; const headless = options.headless ?? isHeadlessEnvironment(); - const isDeviceCodeFlow = callbackPort === null; + const isDeviceCodeFlow = + provider === 'kiro' ? isKiroDeviceCodeMethod(resolvedKiroMethod) : callbackPort === null; + + let authFlag = oauthConfig.authFlag; + if (provider === 'kiro') { + if (!isKiroCLIAuthMethod(resolvedKiroMethod)) { + console.log(fail(`Kiro auth method '${resolvedKiroMethod}' is not supported by CLI flow.`)); + console.log(' Use Dashboard management OAuth for this method.'); + return null; + } + authFlag = getKiroCLIAuthFlag(resolvedKiroMethod); + } // Interactive mode selection for headless environments // Skip if explicit mode flag provided or device code flow (no callback needed) @@ -493,7 +518,7 @@ export async function triggerOAuth( const { binaryPath, tokenDir, configPath } = prepared; // Free callback port if needed (only for authorization code flows) - const localCallbackPort = OAUTH_CALLBACK_PORTS[provider]; + const localCallbackPort = callbackPort; if (localCallbackPort) { const killed = killProcessOnPort(localCallbackPort, verbose); if (killed && verbose) { @@ -502,7 +527,7 @@ export async function triggerOAuth( } // Build args - const args = ['--config', configPath, oauthConfig.authFlag]; + const args = ['--config', configPath, authFlag]; if (headless) { args.push('--no-browser'); } diff --git a/tests/unit/cliproxy/provider-capabilities.test.ts b/tests/unit/cliproxy/provider-capabilities.test.ts index 3434c344..c92c702f 100644 --- a/tests/unit/cliproxy/provider-capabilities.test.ts +++ b/tests/unit/cliproxy/provider-capabilities.test.ts @@ -12,7 +12,14 @@ import { OAUTH_CALLBACK_PORTS as DIAGNOSTIC_CALLBACK_PORTS, OAUTH_FLOW_TYPES, } from '../../../src/management/oauth-port-diagnostics'; -import { OAUTH_CALLBACK_PORTS as AUTH_CALLBACK_PORTS } from '../../../src/cliproxy/auth/auth-types'; +import { + DEFAULT_KIRO_AUTH_METHOD, + getKiroCallbackPort, + getKiroCLIAuthFlag, + normalizeKiroAuthMethod, + OAUTH_CALLBACK_PORTS as AUTH_CALLBACK_PORTS, + toKiroManagementMethod, +} from '../../../src/cliproxy/auth/auth-types'; describe('provider-capabilities', () => { it('keeps canonical provider IDs backward-compatible', () => { @@ -75,4 +82,25 @@ describe('provider-capabilities', () => { expect(AUTH_CALLBACK_PORTS[provider]).toBeUndefined(); } }); + + it('maps Kiro auth methods to upstream CLI/management contracts', () => { + expect(DEFAULT_KIRO_AUTH_METHOD).toBe('aws'); + expect(normalizeKiroAuthMethod()).toBe('aws'); + expect(normalizeKiroAuthMethod('GOOGLE')).toBe('google'); + expect(normalizeKiroAuthMethod('not-valid')).toBe('aws'); + + expect(getKiroCLIAuthFlag('aws')).toBe('--kiro-aws-login'); + expect(getKiroCLIAuthFlag('aws-authcode')).toBe('--kiro-aws-authcode'); + expect(getKiroCLIAuthFlag('google')).toBe('--kiro-google-login'); + + expect(getKiroCallbackPort('aws')).toBeNull(); + expect(getKiroCallbackPort('google')).toBe(9876); + expect(getKiroCallbackPort('github')).toBe(9876); + expect(getKiroCallbackPort('aws-authcode')).toBe(9876); + + expect(toKiroManagementMethod('aws')).toBe('aws'); + expect(toKiroManagementMethod('aws-authcode')).toBe('aws'); + expect(toKiroManagementMethod('google')).toBe('google'); + expect(toKiroManagementMethod('github')).toBe('github'); + }); });