fix(cliproxy): align kiro auth methods with upstream contracts

- add typed Kiro auth-method model and AWS device-code default

- map CLI flags and callback behavior by selected Kiro method

- harden CLI-only method guards and add contract regression tests

Refs #552

Refs #233
This commit is contained in:
Tam Nhu Tran
2026-02-14 13:33:28 +07:00
parent 826a42d8d6
commit fd46ad022a
3 changed files with 132 additions and 7 deletions
+73 -1
View File
@@ -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=<value>
*/
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<CLIProxyProvider, ProviderOAuthConfig> = {
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) */
+30 -5
View File
@@ -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');
}
@@ -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');
});
});