mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 06:20:04 +00:00
fix(kiro): align auth flows with CLIProxyAPIPlus
- auto-select Builder ID for the default Kiro AWS auth flow - support IDC auth flags and callback-based Kiro paste replay - update regression coverage for Kiro auth routing
This commit is contained in:
@@ -757,6 +757,9 @@ async function main(): Promise<void> {
|
|||||||
'--port-forward',
|
'--port-forward',
|
||||||
'--nickname',
|
'--nickname',
|
||||||
'--kiro-auth-method',
|
'--kiro-auth-method',
|
||||||
|
'--kiro-idc-start-url',
|
||||||
|
'--kiro-idc-region',
|
||||||
|
'--kiro-idc-flow',
|
||||||
'--backend',
|
'--backend',
|
||||||
'--proxy-host',
|
'--proxy-host',
|
||||||
'--proxy-port',
|
'--proxy-port',
|
||||||
|
|||||||
@@ -22,14 +22,19 @@ import {
|
|||||||
* - aws-authcode: AWS Builder ID via Authorization Code flow (CLI flag only)
|
* - aws-authcode: AWS Builder ID via Authorization Code flow (CLI flag only)
|
||||||
* - google: Social OAuth via Google
|
* - google: Social OAuth via Google
|
||||||
* - github: Social OAuth via GitHub (management API only)
|
* - github: Social OAuth via GitHub (management API only)
|
||||||
|
* - idc: IAM Identity Center (IDC) via CLI flags with start URL + region
|
||||||
*/
|
*/
|
||||||
export const KIRO_AUTH_METHODS = ['aws', 'aws-authcode', 'google', 'github'] as const;
|
export const KIRO_AUTH_METHODS = ['aws', 'aws-authcode', 'google', 'github', 'idc'] as const;
|
||||||
export type KiroAuthMethod = (typeof KIRO_AUTH_METHODS)[number];
|
export type KiroAuthMethod = (typeof KIRO_AUTH_METHODS)[number];
|
||||||
|
|
||||||
/** CLI binary supports these Kiro methods directly via flags. */
|
/** CLI binary supports these Kiro methods directly via flags. */
|
||||||
export const KIRO_CLI_AUTH_METHODS = ['aws', 'aws-authcode', 'google'] as const;
|
export const KIRO_CLI_AUTH_METHODS = ['aws', 'aws-authcode', 'google', 'idc'] as const;
|
||||||
export type KiroCLIAuthMethod = (typeof KIRO_CLI_AUTH_METHODS)[number];
|
export type KiroCLIAuthMethod = (typeof KIRO_CLI_AUTH_METHODS)[number];
|
||||||
|
|
||||||
|
export const KIRO_IDC_FLOWS = ['authcode', 'device'] as const;
|
||||||
|
export type KiroIDCFlow = (typeof KIRO_IDC_FLOWS)[number];
|
||||||
|
export const DEFAULT_KIRO_IDC_FLOW: KiroIDCFlow = 'authcode';
|
||||||
|
|
||||||
/** Default Kiro method for CCS UX and AWS Organization support. */
|
/** Default Kiro method for CCS UX and AWS Organization support. */
|
||||||
export const DEFAULT_KIRO_AUTH_METHOD: KiroAuthMethod = 'aws';
|
export const DEFAULT_KIRO_AUTH_METHOD: KiroAuthMethod = 'aws';
|
||||||
|
|
||||||
@@ -41,18 +46,40 @@ export function isKiroCLIAuthMethod(value: string): value is KiroCLIAuthMethod {
|
|||||||
return KIRO_CLI_AUTH_METHODS.includes(value as KiroCLIAuthMethod);
|
return KIRO_CLI_AUTH_METHODS.includes(value as KiroCLIAuthMethod);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isKiroIDCFlow(value: string): value is KiroIDCFlow {
|
||||||
|
return KIRO_IDC_FLOWS.includes(value as KiroIDCFlow);
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeKiroAuthMethod(value?: string): KiroAuthMethod {
|
export function normalizeKiroAuthMethod(value?: string): KiroAuthMethod {
|
||||||
if (!value) return DEFAULT_KIRO_AUTH_METHOD;
|
if (!value) return DEFAULT_KIRO_AUTH_METHOD;
|
||||||
const normalized = value.trim().toLowerCase();
|
const normalized = value.trim().toLowerCase();
|
||||||
return isKiroAuthMethod(normalized) ? normalized : DEFAULT_KIRO_AUTH_METHOD;
|
return isKiroAuthMethod(normalized) ? normalized : DEFAULT_KIRO_AUTH_METHOD;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isKiroDeviceCodeMethod(method: KiroAuthMethod): boolean {
|
export function normalizeKiroIDCFlow(value?: string): KiroIDCFlow {
|
||||||
return method === 'aws';
|
if (!value) return DEFAULT_KIRO_IDC_FLOW;
|
||||||
|
const normalized = value.trim().toLowerCase();
|
||||||
|
return isKiroIDCFlow(normalized) ? normalized : DEFAULT_KIRO_IDC_FLOW;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getKiroCallbackPort(method: KiroAuthMethod): number | null {
|
export function isKiroDeviceCodeMethod(
|
||||||
return isKiroDeviceCodeMethod(method) ? null : 9876;
|
method: KiroAuthMethod,
|
||||||
|
options?: { idcFlow?: KiroIDCFlow }
|
||||||
|
): boolean {
|
||||||
|
if (method === 'aws') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (method === 'idc') {
|
||||||
|
return normalizeKiroIDCFlow(options?.idcFlow) === 'device';
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getKiroCallbackPort(
|
||||||
|
method: KiroAuthMethod,
|
||||||
|
options?: { idcFlow?: KiroIDCFlow }
|
||||||
|
): number | null {
|
||||||
|
return isKiroDeviceCodeMethod(method, options) ? null : 9876;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getKiroCLIAuthFlag(method: KiroCLIAuthMethod): string {
|
export function getKiroCLIAuthFlag(method: KiroCLIAuthMethod): string {
|
||||||
@@ -63,19 +90,49 @@ export function getKiroCLIAuthFlag(method: KiroCLIAuthMethod): string {
|
|||||||
return '--kiro-aws-authcode';
|
return '--kiro-aws-authcode';
|
||||||
case 'google':
|
case 'google':
|
||||||
return '--kiro-google-login';
|
return '--kiro-google-login';
|
||||||
|
case 'idc':
|
||||||
|
return '--kiro-idc-login';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getKiroCLIAuthArgs(
|
||||||
|
method: KiroCLIAuthMethod,
|
||||||
|
options?: {
|
||||||
|
idcStartUrl?: string;
|
||||||
|
idcRegion?: string;
|
||||||
|
idcFlow?: KiroIDCFlow;
|
||||||
|
}
|
||||||
|
): string[] {
|
||||||
|
if (method !== 'idc') {
|
||||||
|
return [getKiroCLIAuthFlag(method)];
|
||||||
|
}
|
||||||
|
|
||||||
|
const startUrl = options?.idcStartUrl?.trim();
|
||||||
|
if (!startUrl) {
|
||||||
|
throw new Error('Kiro IDC login requires --kiro-idc-start-url');
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = [getKiroCLIAuthFlag('idc'), '--kiro-idc-start-url', startUrl];
|
||||||
|
const region = options?.idcRegion?.trim();
|
||||||
|
if (region) {
|
||||||
|
args.push('--kiro-idc-region', region);
|
||||||
|
}
|
||||||
|
args.push('--kiro-idc-flow', normalizeKiroIDCFlow(options?.idcFlow));
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kiro method for CLIProxyAPI management endpoint:
|
* Kiro method for CLIProxyAPI management endpoint:
|
||||||
* GET /v0/management/kiro-auth-url?method=<value>
|
* GET /v0/management/kiro-auth-url?method=<value>
|
||||||
*/
|
*/
|
||||||
export function toKiroManagementMethod(method: KiroAuthMethod): 'aws' | 'google' | 'github' {
|
export function toKiroManagementMethod(method: KiroAuthMethod): 'aws' | 'google' | 'github' | null {
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case 'google':
|
case 'google':
|
||||||
return 'google';
|
return 'google';
|
||||||
case 'github':
|
case 'github':
|
||||||
return 'github';
|
return 'github';
|
||||||
|
case 'idc':
|
||||||
|
return null;
|
||||||
case 'aws-authcode':
|
case 'aws-authcode':
|
||||||
return 'aws';
|
return 'aws';
|
||||||
case 'aws':
|
case 'aws':
|
||||||
@@ -258,10 +315,20 @@ export function getManagementAuthUrlPath(provider: CLIProxyProvider): string {
|
|||||||
return `/v0/management/${authUrlProvider}-auth-url?is_webui=true`;
|
return `/v0/management/${authUrlProvider}-auth-url?is_webui=true`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPasteCallbackStartPath(provider: CLIProxyProvider): string {
|
export function getPasteCallbackStartPath(
|
||||||
// Kiro CLI auth methods still use the legacy start route.
|
provider: CLIProxyProvider,
|
||||||
|
options?: { kiroMethod?: KiroAuthMethod }
|
||||||
|
): string | null {
|
||||||
if (provider === 'kiro') {
|
if (provider === 'kiro') {
|
||||||
return `/oauth/${provider}/start`;
|
const kiroMethod = options?.kiroMethod ?? normalizeKiroAuthMethod();
|
||||||
|
if (kiroMethod === 'aws-authcode' || kiroMethod === 'idc') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const managementMethod = toKiroManagementMethod(kiroMethod);
|
||||||
|
if (!managementMethod) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return `${getManagementAuthUrlPath(provider)}&method=${encodeURIComponent(managementMethod)}`;
|
||||||
}
|
}
|
||||||
return getManagementAuthUrlPath(provider);
|
return getManagementAuthUrlPath(provider);
|
||||||
}
|
}
|
||||||
@@ -294,6 +361,12 @@ export interface OAuthOptions {
|
|||||||
acceptAgyRisk?: boolean;
|
acceptAgyRisk?: boolean;
|
||||||
/** Kiro auth method override (CLI + Dashboard parity). */
|
/** Kiro auth method override (CLI + Dashboard parity). */
|
||||||
kiroMethod?: KiroAuthMethod;
|
kiroMethod?: KiroAuthMethod;
|
||||||
|
/** Kiro IDC start URL (required when kiroMethod=idc). */
|
||||||
|
kiroIDCStartUrl?: string;
|
||||||
|
/** Kiro IDC region override. */
|
||||||
|
kiroIDCRegion?: string;
|
||||||
|
/** Kiro IDC flow override (authcode or device). */
|
||||||
|
kiroIDCFlow?: KiroIDCFlow;
|
||||||
/** If true, triggered from Web UI (enables project selection prompt) */
|
/** If true, triggered from Web UI (enables project selection prompt) */
|
||||||
fromUI?: boolean;
|
fromUI?: boolean;
|
||||||
/** If true, use --no-incognito flag (Kiro only - use normal browser instead of incognito) */
|
/** If true, use --no-incognito flag (Kiro only - use normal browser instead of incognito) */
|
||||||
|
|||||||
@@ -32,8 +32,9 @@ import {
|
|||||||
import {
|
import {
|
||||||
OAuthOptions,
|
OAuthOptions,
|
||||||
DEFAULT_KIRO_AUTH_METHOD,
|
DEFAULT_KIRO_AUTH_METHOD,
|
||||||
|
DEFAULT_KIRO_IDC_FLOW,
|
||||||
getKiroCallbackPort,
|
getKiroCallbackPort,
|
||||||
getKiroCLIAuthFlag,
|
getKiroCLIAuthArgs,
|
||||||
isKiroCLIAuthMethod,
|
isKiroCLIAuthMethod,
|
||||||
isKiroDeviceCodeMethod,
|
isKiroDeviceCodeMethod,
|
||||||
getOAuthConfig,
|
getOAuthConfig,
|
||||||
@@ -42,6 +43,7 @@ import {
|
|||||||
getPasteCallbackStartPath,
|
getPasteCallbackStartPath,
|
||||||
getManagementOAuthCallbackPath,
|
getManagementOAuthCallbackPath,
|
||||||
normalizeKiroAuthMethod,
|
normalizeKiroAuthMethod,
|
||||||
|
normalizeKiroIDCFlow,
|
||||||
} from './auth-types';
|
} from './auth-types';
|
||||||
import { isHeadlessEnvironment, killProcessOnPort, showStep } from './environment-detector';
|
import { isHeadlessEnvironment, killProcessOnPort, showStep } from './environment-detector';
|
||||||
import { getProviderTokenDir, isAuthenticated, registerAccountFromToken } from './token-manager';
|
import { getProviderTokenDir, isAuthenticated, registerAccountFromToken } from './token-manager';
|
||||||
@@ -72,15 +74,19 @@ const PASTE_CALLBACK_AUTH_URL_POLL_INTERVAL_MS = 3000;
|
|||||||
|
|
||||||
export async function requestPasteCallbackStart(
|
export async function requestPasteCallbackStart(
|
||||||
provider: CLIProxyProvider,
|
provider: CLIProxyProvider,
|
||||||
target: ProxyTarget
|
target: ProxyTarget,
|
||||||
|
options?: { kiroMethod?: OAuthOptions['kiroMethod'] }
|
||||||
): Promise<PasteCallbackStartData> {
|
): Promise<PasteCallbackStartData> {
|
||||||
const startPath = getPasteCallbackStartPath(provider);
|
const startPath = getPasteCallbackStartPath(provider, {
|
||||||
|
kiroMethod: options?.kiroMethod,
|
||||||
|
});
|
||||||
|
if (!startPath) {
|
||||||
|
throw new Error(
|
||||||
|
`Paste-callback start is not available for ${provider} with the selected method`
|
||||||
|
);
|
||||||
|
}
|
||||||
const response = await fetch(buildProxyUrl(target, startPath), {
|
const response = await fetch(buildProxyUrl(target, startPath), {
|
||||||
...(provider === 'kiro' ? { method: 'POST' } : {}),
|
headers: buildManagementHeaders(target),
|
||||||
headers:
|
|
||||||
provider === 'kiro'
|
|
||||||
? buildManagementHeaders(target, { 'Content-Type': 'application/json' })
|
|
||||||
: buildManagementHeaders(target),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -297,6 +303,46 @@ async function prepareBinary(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildOAuthArgs(
|
||||||
|
provider: CLIProxyProvider,
|
||||||
|
configPath: string,
|
||||||
|
headless: boolean,
|
||||||
|
noIncognito: boolean,
|
||||||
|
options: {
|
||||||
|
kiroMethod?: OAuthOptions['kiroMethod'];
|
||||||
|
kiroIDCStartUrl?: string;
|
||||||
|
kiroIDCRegion?: string;
|
||||||
|
kiroIDCFlow?: OAuthOptions['kiroIDCFlow'];
|
||||||
|
} = {}
|
||||||
|
): string[] {
|
||||||
|
const args = ['--config', configPath];
|
||||||
|
|
||||||
|
if (provider === 'kiro') {
|
||||||
|
const method = normalizeKiroAuthMethod(options.kiroMethod);
|
||||||
|
if (!isKiroCLIAuthMethod(method)) {
|
||||||
|
throw new Error(`Kiro auth method '${method}' is not supported by CLI flow.`);
|
||||||
|
}
|
||||||
|
args.push(
|
||||||
|
...getKiroCLIAuthArgs(method, {
|
||||||
|
idcStartUrl: options.kiroIDCStartUrl,
|
||||||
|
idcRegion: options.kiroIDCRegion,
|
||||||
|
idcFlow: options.kiroIDCFlow,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
args.push(getOAuthConfig(provider).authFlag);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headless) {
|
||||||
|
args.push('--no-browser');
|
||||||
|
}
|
||||||
|
if (provider === 'kiro' && noIncognito) {
|
||||||
|
args.push('--no-incognito');
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle paste-callback mode: show auth URL, prompt for callback paste
|
* Handle paste-callback mode: show auth URL, prompt for callback paste
|
||||||
* Uses proxy target resolver to connect to correct CLIProxyAPI instance (local or remote)
|
* Uses proxy target resolver to connect to correct CLIProxyAPI instance (local or remote)
|
||||||
@@ -307,7 +353,8 @@ async function handlePasteCallbackMode(
|
|||||||
verbose: boolean,
|
verbose: boolean,
|
||||||
tokenDir: string,
|
tokenDir: string,
|
||||||
nickname?: string,
|
nickname?: string,
|
||||||
expectedAccountId?: string
|
expectedAccountId?: string,
|
||||||
|
options?: { kiroMethod?: OAuthOptions['kiroMethod'] }
|
||||||
): Promise<AccountInfo | null> {
|
): Promise<AccountInfo | null> {
|
||||||
// Resolve CLIProxyAPI target (local or remote based on config)
|
// Resolve CLIProxyAPI target (local or remote based on config)
|
||||||
const target = getProxyTarget();
|
const target = getProxyTarget();
|
||||||
@@ -318,12 +365,13 @@ async function handlePasteCallbackMode(
|
|||||||
console.log(info(`Starting ${oauthConfig.displayName} OAuth (paste-callback mode)...`));
|
console.log(info(`Starting ${oauthConfig.displayName} OAuth (paste-callback mode)...`));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Request auth URL from CLIProxyAPI.
|
// Request auth URL from CLIProxyAPI management endpoints when the selected
|
||||||
// Kiro keeps its legacy start route because CLI auth methods do not share the generic
|
// provider/method supports the manual start-url contract.
|
||||||
// management auth-url contract used by providers like Claude.
|
|
||||||
let startData: PasteCallbackStartData;
|
let startData: PasteCallbackStartData;
|
||||||
try {
|
try {
|
||||||
startData = await requestPasteCallbackStart(provider, target);
|
startData = await requestPasteCallbackStart(provider, target, {
|
||||||
|
kiroMethod: options?.kiroMethod,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const startError = (error as Error).message;
|
const startError = (error as Error).message;
|
||||||
console.log(fail('Failed to start OAuth flow'));
|
console.log(fail('Failed to start OAuth flow'));
|
||||||
@@ -475,6 +523,8 @@ export async function triggerOAuth(
|
|||||||
const { nickname } = options;
|
const { nickname } = options;
|
||||||
const resolvedKiroMethod =
|
const resolvedKiroMethod =
|
||||||
provider === 'kiro' ? normalizeKiroAuthMethod(options.kiroMethod) : DEFAULT_KIRO_AUTH_METHOD;
|
provider === 'kiro' ? normalizeKiroAuthMethod(options.kiroMethod) : DEFAULT_KIRO_AUTH_METHOD;
|
||||||
|
const resolvedKiroIDCFlow =
|
||||||
|
provider === 'kiro' ? normalizeKiroIDCFlow(options.kiroIDCFlow) : DEFAULT_KIRO_IDC_FLOW;
|
||||||
|
|
||||||
if (provider === 'agy') {
|
if (provider === 'agy') {
|
||||||
if (fromUI && !acceptAgyRisk) {
|
if (fromUI && !acceptAgyRisk) {
|
||||||
@@ -505,19 +555,6 @@ export async function triggerOAuth(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle paste-callback mode
|
|
||||||
if (options.pasteCallback) {
|
|
||||||
const tokenDir = getProviderTokenDir(provider);
|
|
||||||
return handlePasteCallbackMode(
|
|
||||||
provider,
|
|
||||||
oauthConfig,
|
|
||||||
verbose,
|
|
||||||
tokenDir,
|
|
||||||
nickname,
|
|
||||||
existingNameMatch?.id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle --import flag: skip OAuth and import from Kiro IDE directly
|
// Handle --import flag: skip OAuth and import from Kiro IDE directly
|
||||||
if (options.import && provider === 'kiro') {
|
if (options.import && provider === 'kiro') {
|
||||||
const tokenDir = getProviderTokenDir(provider);
|
const tokenDir = getProviderTokenDir(provider);
|
||||||
@@ -535,20 +572,24 @@ export async function triggerOAuth(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const callbackPort =
|
const callbackPort =
|
||||||
provider === 'kiro' ? getKiroCallbackPort(resolvedKiroMethod) : OAUTH_PORTS[provider];
|
provider === 'kiro'
|
||||||
|
? getKiroCallbackPort(resolvedKiroMethod, { idcFlow: resolvedKiroIDCFlow })
|
||||||
|
: OAUTH_PORTS[provider];
|
||||||
const isCLI = !fromUI;
|
const isCLI = !fromUI;
|
||||||
const headless = options.headless ?? isHeadlessEnvironment();
|
const headless = options.headless ?? isHeadlessEnvironment();
|
||||||
const isDeviceCodeFlow =
|
const isDeviceCodeFlow =
|
||||||
provider === 'kiro' ? isKiroDeviceCodeMethod(resolvedKiroMethod) : callbackPort === null;
|
provider === 'kiro'
|
||||||
|
? isKiroDeviceCodeMethod(resolvedKiroMethod, { idcFlow: resolvedKiroIDCFlow })
|
||||||
|
: callbackPort === null;
|
||||||
|
const useKiroLocalPasteCallback =
|
||||||
|
options.pasteCallback === true && provider === 'kiro' && !isDeviceCodeFlow;
|
||||||
|
const useKiroDirectCliFlow =
|
||||||
|
provider === 'kiro' && (isDeviceCodeFlow || useKiroLocalPasteCallback);
|
||||||
|
|
||||||
let authFlag = oauthConfig.authFlag;
|
if (provider === 'kiro' && !isKiroCLIAuthMethod(resolvedKiroMethod)) {
|
||||||
if (provider === 'kiro') {
|
console.log(fail(`Kiro auth method '${resolvedKiroMethod}' is not supported by CLI flow.`));
|
||||||
if (!isKiroCLIAuthMethod(resolvedKiroMethod)) {
|
console.log(' Use Dashboard management OAuth for this method.');
|
||||||
console.log(fail(`Kiro auth method '${resolvedKiroMethod}' is not supported by CLI flow.`));
|
return null;
|
||||||
console.log(' Use Dashboard management OAuth for this method.');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
authFlag = getKiroCLIAuthFlag(resolvedKiroMethod);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interactive mode selection for headless environments
|
// Interactive mode selection for headless environments
|
||||||
@@ -595,6 +636,19 @@ export async function triggerOAuth(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.pasteCallback && !useKiroDirectCliFlow) {
|
||||||
|
const tokenDir = getProviderTokenDir(provider);
|
||||||
|
return handlePasteCallbackMode(
|
||||||
|
provider,
|
||||||
|
oauthConfig,
|
||||||
|
verbose,
|
||||||
|
tokenDir,
|
||||||
|
nickname,
|
||||||
|
existingNameMatch?.id,
|
||||||
|
{ kiroMethod: provider === 'kiro' ? resolvedKiroMethod : undefined }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Pre-flight checks (skip for device code flows which don't need callback ports)
|
// Pre-flight checks (skip for device code flows which don't need callback ports)
|
||||||
if (!isDeviceCodeFlow && !(await runPreflightChecks(provider, oauthConfig))) {
|
if (!isDeviceCodeFlow && !(await runPreflightChecks(provider, oauthConfig))) {
|
||||||
return null;
|
return null;
|
||||||
@@ -617,14 +671,18 @@ export async function triggerOAuth(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build args
|
const processHeadless = options.pasteCallback && provider === 'kiro' ? true : headless;
|
||||||
const args = ['--config', configPath, authFlag];
|
let args: string[];
|
||||||
if (headless) {
|
try {
|
||||||
args.push('--no-browser');
|
args = buildOAuthArgs(provider, configPath, processHeadless, noIncognito, {
|
||||||
}
|
kiroMethod: provider === 'kiro' ? resolvedKiroMethod : undefined,
|
||||||
// Kiro-specific: --no-incognito to use normal browser (saves login credentials)
|
kiroIDCStartUrl: options.kiroIDCStartUrl,
|
||||||
if (provider === 'kiro' && noIncognito) {
|
kiroIDCRegion: options.kiroIDCRegion,
|
||||||
args.push('--no-incognito');
|
kiroIDCFlow: provider === 'kiro' ? resolvedKiroIDCFlow : undefined,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log(fail((error as Error).message));
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show step based on flow type
|
// Show step based on flow type
|
||||||
@@ -636,7 +694,14 @@ export async function triggerOAuth(
|
|||||||
showStep(2, 4, 'progress', `Starting callback server on port ${callbackPort}...`);
|
showStep(2, 4, 'progress', `Starting callback server on port ${callbackPort}...`);
|
||||||
|
|
||||||
// Show headless instructions (only for authorization code flows)
|
// Show headless instructions (only for authorization code flows)
|
||||||
if (headless) {
|
if (useKiroLocalPasteCallback) {
|
||||||
|
console.log('');
|
||||||
|
console.log(info('Paste-callback mode enabled for Kiro CLI auth.'));
|
||||||
|
console.log(
|
||||||
|
' CCS will print the authorization URL and wait for you to paste the final callback URL.'
|
||||||
|
);
|
||||||
|
console.log('');
|
||||||
|
} else 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.`);
|
||||||
@@ -656,11 +721,14 @@ export async function triggerOAuth(
|
|||||||
tokenDir,
|
tokenDir,
|
||||||
oauthConfig,
|
oauthConfig,
|
||||||
callbackPort,
|
callbackPort,
|
||||||
headless,
|
headless: processHeadless,
|
||||||
verbose,
|
verbose,
|
||||||
isCLI,
|
isCLI,
|
||||||
nickname,
|
nickname,
|
||||||
expectedAccountId: existingNameMatch?.id,
|
expectedAccountId: existingNameMatch?.id,
|
||||||
|
authFlowType: isDeviceCodeFlow ? 'device_code' : 'authorization_code',
|
||||||
|
kiroMethod: provider === 'kiro' ? resolvedKiroMethod : undefined,
|
||||||
|
manualCallback: useKiroLocalPasteCallback,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show hint for Kiro users about --no-incognito option (first-time auth only)
|
// Show hint for Kiro users about --no-incognito option (first-time auth only)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
type GCloudProject,
|
type GCloudProject,
|
||||||
type ProjectSelectionPrompt,
|
type ProjectSelectionPrompt,
|
||||||
} from '../project-selection-handler';
|
} from '../project-selection-handler';
|
||||||
import { ProviderOAuthConfig } from './auth-types';
|
import { KiroAuthMethod, ProviderOAuthConfig } from './auth-types';
|
||||||
import { getTimeoutTroubleshooting, showStep } from './environment-detector';
|
import { getTimeoutTroubleshooting, showStep } from './environment-detector';
|
||||||
import { isAuthenticated, registerAccountFromToken } from './token-manager';
|
import { isAuthenticated, registerAccountFromToken } from './token-manager';
|
||||||
import {
|
import {
|
||||||
@@ -51,6 +51,9 @@ export interface OAuthProcessOptions {
|
|||||||
isCLI: boolean;
|
isCLI: boolean;
|
||||||
nickname?: string;
|
nickname?: string;
|
||||||
expectedAccountId?: string;
|
expectedAccountId?: string;
|
||||||
|
authFlowType?: 'device_code' | 'authorization_code';
|
||||||
|
kiroMethod?: KiroAuthMethod;
|
||||||
|
manualCallback?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Internal state for OAuth process */
|
/** Internal state for OAuth process */
|
||||||
@@ -66,6 +69,8 @@ interface ProcessState {
|
|||||||
deviceCodeDisplayed: boolean;
|
deviceCodeDisplayed: boolean;
|
||||||
/** The user code to enter at verification URL */
|
/** The user code to enter at verification URL */
|
||||||
userCode: string | null;
|
userCode: string | null;
|
||||||
|
kiroMethodSelectionHandled: boolean;
|
||||||
|
manualCallbackPrompted: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -106,6 +111,92 @@ async function handleProjectSelection(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveAuthFlowType(options: OAuthProcessOptions): 'device_code' | 'authorization_code' {
|
||||||
|
return options.authFlowType || OAUTH_FLOW_TYPES[options.provider] || 'authorization_code';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function promptManualCallbackUrl(displayName: string): Promise<string | null> {
|
||||||
|
const readline = await import('readline');
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise<string | null>((resolve) => {
|
||||||
|
let settled = false;
|
||||||
|
|
||||||
|
rl.on('close', () => {
|
||||||
|
if (!settled) {
|
||||||
|
settled = true;
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
console.log(info(`${displayName} is waiting for the OAuth callback.`));
|
||||||
|
console.log('Paste the full callback URL after you finish the login in your browser.');
|
||||||
|
rl.question('> ', (answer) => {
|
||||||
|
settled = true;
|
||||||
|
rl.close();
|
||||||
|
resolve(answer.trim() || null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function replayManualCallback(
|
||||||
|
oauthConfig: ProviderOAuthConfig,
|
||||||
|
authProcess: ChildProcess,
|
||||||
|
output: string,
|
||||||
|
verbose: boolean
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (!output.includes('http://') && !output.includes('https://')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const callbackUrl = await promptManualCallbackUrl(oauthConfig.displayName);
|
||||||
|
if (!callbackUrl) {
|
||||||
|
console.log(info('Cancelled'));
|
||||||
|
killWithEscalation(authProcess);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed: URL;
|
||||||
|
try {
|
||||||
|
parsed = new URL(callbackUrl);
|
||||||
|
} catch {
|
||||||
|
console.log(fail('Invalid callback URL format'));
|
||||||
|
killWithEscalation(authProcess);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parsed.searchParams.get('code')) {
|
||||||
|
console.log(fail('Invalid callback URL: missing code parameter'));
|
||||||
|
killWithEscalation(authProcess);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(info('Replaying callback to the local auth server...'));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(callbackUrl);
|
||||||
|
if (!response.ok && response.status >= 400) {
|
||||||
|
console.log(fail(`OAuth callback failed with status ${response.status}`));
|
||||||
|
killWithEscalation(authProcess);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
console.log(ok('Callback submitted. Waiting for token exchange...'));
|
||||||
|
} catch (error) {
|
||||||
|
if (verbose) {
|
||||||
|
console.log(fail(`Failed to replay callback: ${(error as Error).message}`));
|
||||||
|
} else {
|
||||||
|
console.log(fail('Failed to replay callback to the local auth server'));
|
||||||
|
}
|
||||||
|
killWithEscalation(authProcess);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle stdout data from OAuth process
|
* Handle stdout data from OAuth process
|
||||||
*/
|
*/
|
||||||
@@ -119,10 +210,20 @@ async function handleStdout(
|
|||||||
log(`stdout: ${output.trim()}`);
|
log(`stdout: ${output.trim()}`);
|
||||||
state.accumulatedOutput += output;
|
state.accumulatedOutput += output;
|
||||||
|
|
||||||
// H4: Use explicit flow type from OAUTH_FLOW_TYPES instead of null port check
|
const flowType = resolveAuthFlowType(options);
|
||||||
const flowType = OAUTH_FLOW_TYPES[options.provider] || 'authorization_code';
|
|
||||||
const isDeviceCodeFlow = flowType === 'device_code';
|
const isDeviceCodeFlow = flowType === 'device_code';
|
||||||
|
|
||||||
|
if (
|
||||||
|
options.provider === 'kiro' &&
|
||||||
|
options.kiroMethod === 'aws' &&
|
||||||
|
!state.kiroMethodSelectionHandled &&
|
||||||
|
state.accumulatedOutput.includes('Select login method')
|
||||||
|
) {
|
||||||
|
state.kiroMethodSelectionHandled = true;
|
||||||
|
authProcess.stdin?.write('1\n');
|
||||||
|
log('Auto-selected Kiro Builder ID flow');
|
||||||
|
}
|
||||||
|
|
||||||
// Parse project list when available
|
// Parse project list when available
|
||||||
if (isProjectList(state.accumulatedOutput) && state.parsedProjects.length === 0) {
|
if (isProjectList(state.accumulatedOutput) && state.parsedProjects.length === 0) {
|
||||||
state.parsedProjects = parseProjectList(state.accumulatedOutput);
|
state.parsedProjects = parseProjectList(state.accumulatedOutput);
|
||||||
@@ -198,6 +299,11 @@ async function handleStdout(
|
|||||||
console.log(` ${urlMatch[0]}`);
|
console.log(` ${urlMatch[0]}`);
|
||||||
console.log('');
|
console.log('');
|
||||||
state.urlDisplayed = true;
|
state.urlDisplayed = true;
|
||||||
|
|
||||||
|
if (options.manualCallback && !state.manualCallbackPrompted) {
|
||||||
|
state.manualCallbackPrompted = true;
|
||||||
|
await replayManualCallback(options.oauthConfig, authProcess, urlMatch[0], options.verbose);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -386,14 +492,17 @@ export function executeOAuthProcess(options: OAuthProcessOptions): Promise<Accou
|
|||||||
};
|
};
|
||||||
|
|
||||||
return new Promise<AccountInfo | null>((resolve) => {
|
return new Promise<AccountInfo | null>((resolve) => {
|
||||||
// H4: Use explicit flow type from OAUTH_FLOW_TYPES instead of null port check
|
const flowType = resolveAuthFlowType(options);
|
||||||
const flowType = OAUTH_FLOW_TYPES[provider] || 'authorization_code';
|
|
||||||
const isDeviceCodeFlow = flowType === 'device_code';
|
const isDeviceCodeFlow = flowType === 'device_code';
|
||||||
|
|
||||||
// H6: TTY detection - only inherit stdin if TTY available (prevents issues in CI/piped scripts)
|
// Device-code flows can usually inherit stdin, but Kiro's default AWS flow now
|
||||||
// Device Code flows may need interactive stdin for email/prompts
|
// prints an intermediate Builder ID vs IDC selector that CCS auto-answers.
|
||||||
// Authorization Code flows need piped stdin for project selection
|
const stdinMode =
|
||||||
const stdinMode = isDeviceCodeFlow && process.stdin.isTTY ? 'inherit' : 'pipe';
|
isDeviceCodeFlow &&
|
||||||
|
process.stdin.isTTY &&
|
||||||
|
!(provider === 'kiro' && options.kiroMethod === 'aws')
|
||||||
|
? 'inherit'
|
||||||
|
: 'pipe';
|
||||||
|
|
||||||
const authProcess = spawn(binaryPath, args, {
|
const authProcess = spawn(binaryPath, args, {
|
||||||
stdio: [stdinMode, 'pipe', 'pipe'],
|
stdio: [stdinMode, 'pipe', 'pipe'],
|
||||||
@@ -424,6 +533,8 @@ export function executeOAuthProcess(options: OAuthProcessOptions): Promise<Accou
|
|||||||
sessionId: generateSessionId(),
|
sessionId: generateSessionId(),
|
||||||
deviceCodeDisplayed: false,
|
deviceCodeDisplayed: false,
|
||||||
userCode: null,
|
userCode: null,
|
||||||
|
kiroMethodSelectionHandled: false,
|
||||||
|
manualCallbackPrompted: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Register session for cancellation support
|
// Register session for cancellation support
|
||||||
|
|||||||
@@ -65,7 +65,14 @@ import {
|
|||||||
} from '../../utils/image-analysis';
|
} from '../../utils/image-analysis';
|
||||||
import { loadOrCreateUnifiedConfig, getThinkingConfig } from '../../config/unified-config-loader';
|
import { loadOrCreateUnifiedConfig, getThinkingConfig } from '../../config/unified-config-loader';
|
||||||
import { HttpsTunnelProxy } from '../https-tunnel-proxy';
|
import { HttpsTunnelProxy } from '../https-tunnel-proxy';
|
||||||
import { isKiroAuthMethod, KiroAuthMethod, normalizeKiroAuthMethod } from '../auth/auth-types';
|
import {
|
||||||
|
isKiroAuthMethod,
|
||||||
|
isKiroIDCFlow,
|
||||||
|
KiroAuthMethod,
|
||||||
|
KiroIDCFlow,
|
||||||
|
normalizeKiroAuthMethod,
|
||||||
|
normalizeKiroIDCFlow,
|
||||||
|
} from '../auth/auth-types';
|
||||||
import { resolveProfileContinuityInheritance } from '../../auth/profile-continuity-inheritance';
|
import { resolveProfileContinuityInheritance } from '../../auth/profile-continuity-inheritance';
|
||||||
|
|
||||||
// Import modular components
|
// Import modular components
|
||||||
@@ -344,26 +351,115 @@ export async function execClaudeWithCLIProxy(
|
|||||||
const rawMethod = argsWithoutProxy[kiroMethodIdx + 1];
|
const rawMethod = argsWithoutProxy[kiroMethodIdx + 1];
|
||||||
if (!rawMethod || rawMethod.startsWith('-')) {
|
if (!rawMethod || rawMethod.startsWith('-')) {
|
||||||
console.error(fail('--kiro-auth-method requires a value'));
|
console.error(fail('--kiro-auth-method requires a value'));
|
||||||
console.error(' Supported values: aws, aws-authcode, google, github');
|
console.error(' Supported values: aws, aws-authcode, google, github, idc');
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const normalized = rawMethod.trim().toLowerCase();
|
const normalized = rawMethod.trim().toLowerCase();
|
||||||
if (!isKiroAuthMethod(normalized)) {
|
if (!isKiroAuthMethod(normalized)) {
|
||||||
console.error(fail(`Invalid --kiro-auth-method value: ${rawMethod}`));
|
console.error(fail(`Invalid --kiro-auth-method value: ${rawMethod}`));
|
||||||
console.error(' Supported values: aws, aws-authcode, google, github');
|
console.error(' Supported values: aws, aws-authcode, google, github, idc');
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
kiroAuthMethod = normalizeKiroAuthMethod(normalized);
|
kiroAuthMethod = normalizeKiroAuthMethod(normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let kiroIDCStartUrl: string | undefined;
|
||||||
|
const kiroIDCStartUrlIdx = argsWithoutProxy.indexOf('--kiro-idc-start-url');
|
||||||
|
if (
|
||||||
|
kiroIDCStartUrlIdx !== -1 &&
|
||||||
|
argsWithoutProxy[kiroIDCStartUrlIdx + 1] &&
|
||||||
|
!argsWithoutProxy[kiroIDCStartUrlIdx + 1].startsWith('-')
|
||||||
|
) {
|
||||||
|
kiroIDCStartUrl = argsWithoutProxy[kiroIDCStartUrlIdx + 1].trim();
|
||||||
|
} else if (kiroIDCStartUrlIdx !== -1) {
|
||||||
|
console.error(fail('--kiro-idc-start-url requires a value'));
|
||||||
|
process.exitCode = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let kiroIDCRegion: string | undefined;
|
||||||
|
const kiroIDCRegionIdx = argsWithoutProxy.indexOf('--kiro-idc-region');
|
||||||
|
if (
|
||||||
|
kiroIDCRegionIdx !== -1 &&
|
||||||
|
argsWithoutProxy[kiroIDCRegionIdx + 1] &&
|
||||||
|
!argsWithoutProxy[kiroIDCRegionIdx + 1].startsWith('-')
|
||||||
|
) {
|
||||||
|
kiroIDCRegion = argsWithoutProxy[kiroIDCRegionIdx + 1].trim();
|
||||||
|
} else if (kiroIDCRegionIdx !== -1) {
|
||||||
|
console.error(fail('--kiro-idc-region requires a value'));
|
||||||
|
process.exitCode = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let kiroIDCFlow: KiroIDCFlow | undefined;
|
||||||
|
const kiroIDCFlowIdx = argsWithoutProxy.indexOf('--kiro-idc-flow');
|
||||||
|
if (kiroIDCFlowIdx !== -1) {
|
||||||
|
const rawFlow = argsWithoutProxy[kiroIDCFlowIdx + 1];
|
||||||
|
if (!rawFlow || rawFlow.startsWith('-')) {
|
||||||
|
console.error(fail('--kiro-idc-flow requires a value'));
|
||||||
|
console.error(' Supported values: authcode, device');
|
||||||
|
process.exitCode = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const normalized = rawFlow.trim().toLowerCase();
|
||||||
|
if (!isKiroIDCFlow(normalized)) {
|
||||||
|
console.error(fail(`Invalid --kiro-idc-flow value: ${rawFlow}`));
|
||||||
|
console.error(' Supported values: authcode, device');
|
||||||
|
process.exitCode = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
kiroIDCFlow = normalizeKiroIDCFlow(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
if (kiroAuthMethod && provider !== 'kiro' && !compositeProviders.includes('kiro')) {
|
if (kiroAuthMethod && provider !== 'kiro' && !compositeProviders.includes('kiro')) {
|
||||||
console.error(fail('--kiro-auth-method is only valid for ccs kiro'));
|
console.error(fail('--kiro-auth-method is only valid for ccs kiro'));
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(kiroIDCStartUrl || kiroIDCRegion || kiroIDCFlow) &&
|
||||||
|
provider !== 'kiro' &&
|
||||||
|
!compositeProviders.includes('kiro')
|
||||||
|
) {
|
||||||
|
console.error(
|
||||||
|
fail(
|
||||||
|
'--kiro-idc-start-url, --kiro-idc-region, and --kiro-idc-flow are only valid for ccs kiro'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
process.exitCode = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!kiroAuthMethod && (kiroIDCStartUrl || kiroIDCRegion || kiroIDCFlow)) {
|
||||||
|
kiroAuthMethod = 'idc';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kiroAuthMethod === 'idc' && !kiroIDCStartUrl) {
|
||||||
|
console.error(fail('Kiro IDC login requires --kiro-idc-start-url'));
|
||||||
|
console.error(
|
||||||
|
' Example: ccs kiro --auth --kiro-auth-method idc --kiro-idc-start-url https://d-xxx.awsapps.com/start'
|
||||||
|
);
|
||||||
|
process.exitCode = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
kiroAuthMethod &&
|
||||||
|
kiroAuthMethod !== 'idc' &&
|
||||||
|
(kiroIDCStartUrl || kiroIDCRegion || kiroIDCFlow)
|
||||||
|
) {
|
||||||
|
console.error(
|
||||||
|
fail(
|
||||||
|
'--kiro-idc-start-url, --kiro-idc-region, and --kiro-idc-flow require --kiro-auth-method idc'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
process.exitCode = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Parse --thinking / --effort flags (aliases; first occurrence wins)
|
// Parse --thinking / --effort flags (aliases; first occurrence wins)
|
||||||
const thinkingParse = parseThinkingOverride(argsWithoutProxy);
|
const thinkingParse = parseThinkingOverride(argsWithoutProxy);
|
||||||
if (thinkingParse.error) {
|
if (thinkingParse.error) {
|
||||||
@@ -533,6 +629,9 @@ export async function execClaudeWithCLIProxy(
|
|||||||
verbose,
|
verbose,
|
||||||
import: true,
|
import: true,
|
||||||
...(kiroAuthMethod ? { kiroMethod: kiroAuthMethod } : {}),
|
...(kiroAuthMethod ? { kiroMethod: kiroAuthMethod } : {}),
|
||||||
|
...(kiroIDCStartUrl ? { kiroIDCStartUrl } : {}),
|
||||||
|
...(kiroIDCRegion ? { kiroIDCRegion } : {}),
|
||||||
|
...(kiroIDCFlow ? { kiroIDCFlow } : {}),
|
||||||
...(setNickname ? { nickname: setNickname } : {}),
|
...(setNickname ? { nickname: setNickname } : {}),
|
||||||
});
|
});
|
||||||
if (!authSuccess) {
|
if (!authSuccess) {
|
||||||
@@ -597,6 +696,9 @@ export async function execClaudeWithCLIProxy(
|
|||||||
add: addAccount,
|
add: addAccount,
|
||||||
...(acceptAgyRisk ? { acceptAgyRisk: true } : {}),
|
...(acceptAgyRisk ? { acceptAgyRisk: true } : {}),
|
||||||
...(kiroAuthMethod && p === 'kiro' ? { kiroMethod: kiroAuthMethod } : {}),
|
...(kiroAuthMethod && p === 'kiro' ? { kiroMethod: kiroAuthMethod } : {}),
|
||||||
|
...(kiroIDCStartUrl && p === 'kiro' ? { kiroIDCStartUrl } : {}),
|
||||||
|
...(kiroIDCRegion && p === 'kiro' ? { kiroIDCRegion } : {}),
|
||||||
|
...(kiroIDCFlow && p === 'kiro' ? { kiroIDCFlow } : {}),
|
||||||
...(forceHeadless ? { headless: true } : {}),
|
...(forceHeadless ? { headless: true } : {}),
|
||||||
...(setNickname ? { nickname: setNickname } : {}),
|
...(setNickname ? { nickname: setNickname } : {}),
|
||||||
...(noIncognito ? { noIncognito: true } : {}),
|
...(noIncognito ? { noIncognito: true } : {}),
|
||||||
@@ -639,6 +741,9 @@ export async function execClaudeWithCLIProxy(
|
|||||||
add: addAccount,
|
add: addAccount,
|
||||||
...(acceptAgyRisk ? { acceptAgyRisk: true } : {}),
|
...(acceptAgyRisk ? { acceptAgyRisk: true } : {}),
|
||||||
...(kiroAuthMethod ? { kiroMethod: kiroAuthMethod } : {}),
|
...(kiroAuthMethod ? { kiroMethod: kiroAuthMethod } : {}),
|
||||||
|
...(kiroIDCStartUrl ? { kiroIDCStartUrl } : {}),
|
||||||
|
...(kiroIDCRegion ? { kiroIDCRegion } : {}),
|
||||||
|
...(kiroIDCFlow ? { kiroIDCFlow } : {}),
|
||||||
...(forceHeadless ? { headless: true } : {}),
|
...(forceHeadless ? { headless: true } : {}),
|
||||||
...(setNickname ? { nickname: setNickname } : {}),
|
...(setNickname ? { nickname: setNickname } : {}),
|
||||||
...(noIncognito ? { noIncognito: true } : {}),
|
...(noIncognito ? { noIncognito: true } : {}),
|
||||||
@@ -1052,6 +1157,9 @@ export async function execClaudeWithCLIProxy(
|
|||||||
'--use',
|
'--use',
|
||||||
'--nickname',
|
'--nickname',
|
||||||
'--kiro-auth-method',
|
'--kiro-auth-method',
|
||||||
|
'--kiro-idc-start-url',
|
||||||
|
'--kiro-idc-region',
|
||||||
|
'--kiro-idc-flow',
|
||||||
'--thinking',
|
'--thinking',
|
||||||
'--effort',
|
'--effort',
|
||||||
'--1m',
|
'--1m',
|
||||||
@@ -1073,6 +1181,9 @@ export async function execClaudeWithCLIProxy(
|
|||||||
argsWithoutProxy[idx - 1] === '--use' ||
|
argsWithoutProxy[idx - 1] === '--use' ||
|
||||||
argsWithoutProxy[idx - 1] === '--nickname' ||
|
argsWithoutProxy[idx - 1] === '--nickname' ||
|
||||||
argsWithoutProxy[idx - 1] === '--kiro-auth-method' ||
|
argsWithoutProxy[idx - 1] === '--kiro-auth-method' ||
|
||||||
|
argsWithoutProxy[idx - 1] === '--kiro-idc-start-url' ||
|
||||||
|
argsWithoutProxy[idx - 1] === '--kiro-idc-region' ||
|
||||||
|
argsWithoutProxy[idx - 1] === '--kiro-idc-flow' ||
|
||||||
argsWithoutProxy[idx - 1] === '--thinking' ||
|
argsWithoutProxy[idx - 1] === '--thinking' ||
|
||||||
argsWithoutProxy[idx - 1] === '--effort'
|
argsWithoutProxy[idx - 1] === '--effort'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -220,7 +220,15 @@ function getSuggestionsForCommand(tokensBeforeCurrent: string[]): CompletionSugg
|
|||||||
if (command === 'kiro') {
|
if (command === 'kiro') {
|
||||||
return completeSubcommands(
|
return completeSubcommands(
|
||||||
[],
|
[],
|
||||||
[...PROVIDER_FLAGS, '--kiro-auth-method', '--import', '--incognito']
|
[
|
||||||
|
...PROVIDER_FLAGS,
|
||||||
|
'--kiro-auth-method',
|
||||||
|
'--kiro-idc-start-url',
|
||||||
|
'--kiro-idc-region',
|
||||||
|
'--kiro-idc-flow',
|
||||||
|
'--import',
|
||||||
|
'--incognito',
|
||||||
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return completeSubcommands([], PROVIDER_FLAGS);
|
return completeSubcommands([], PROVIDER_FLAGS);
|
||||||
|
|||||||
@@ -262,6 +262,9 @@ export function getStartUrlUnsupportedReason(
|
|||||||
): string | null {
|
): string | null {
|
||||||
if (provider === 'kiro') {
|
if (provider === 'kiro') {
|
||||||
const kiroMethod = options?.kiroMethod ?? normalizeKiroAuthMethod();
|
const kiroMethod = options?.kiroMethod ?? normalizeKiroAuthMethod();
|
||||||
|
if (kiroMethod === 'idc') {
|
||||||
|
return "Kiro method 'idc' uses CLI auth flow. Use /api/cliproxy/auth/kiro/start instead.";
|
||||||
|
}
|
||||||
if (kiroMethod === 'aws-authcode') {
|
if (kiroMethod === 'aws-authcode') {
|
||||||
return "Kiro method 'aws-authcode' uses CLI auth flow. Use /api/cliproxy/auth/kiro/start instead.";
|
return "Kiro method 'aws-authcode' uses CLI auth flow. Use /api/cliproxy/auth/kiro/start instead.";
|
||||||
}
|
}
|
||||||
@@ -615,7 +618,7 @@ router.post('/:provider/start', async (req: Request, res: Response): Promise<voi
|
|||||||
|
|
||||||
if (provider === 'kiro' && invalidKiroMethod) {
|
if (provider === 'kiro' && invalidKiroMethod) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
error: 'Invalid kiroMethod. Supported: aws, aws-authcode, google, github',
|
error: 'Invalid kiroMethod. Supported: aws, aws-authcode, google, github, idc',
|
||||||
code: 'INVALID_KIRO_METHOD',
|
code: 'INVALID_KIRO_METHOD',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -828,7 +831,7 @@ router.post('/:provider/start-url', async (req: Request, res: Response): Promise
|
|||||||
|
|
||||||
if (provider === 'kiro' && invalidKiroMethod) {
|
if (provider === 'kiro' && invalidKiroMethod) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
error: 'Invalid kiroMethod. Supported: aws, aws-authcode, google, github',
|
error: 'Invalid kiroMethod. Supported: aws, aws-authcode, google, github, idc',
|
||||||
code: 'INVALID_KIRO_METHOD',
|
code: 'INVALID_KIRO_METHOD',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -867,9 +870,10 @@ router.post('/:provider/start-url', async (req: Request, res: Response): Promise
|
|||||||
try {
|
try {
|
||||||
const authUrlProvider =
|
const authUrlProvider =
|
||||||
CLIPROXY_AUTH_URL_PROVIDER_MAP[provider as CLIProxyProvider] || provider;
|
CLIPROXY_AUTH_URL_PROVIDER_MAP[provider as CLIProxyProvider] || provider;
|
||||||
|
const kiroManagementMethod = provider === 'kiro' ? toKiroManagementMethod(kiroMethod) : null;
|
||||||
const kiroQuery =
|
const kiroQuery =
|
||||||
provider === 'kiro'
|
provider === 'kiro' && kiroManagementMethod
|
||||||
? `&method=${encodeURIComponent(toKiroManagementMethod(kiroMethod))}`
|
? `&method=${encodeURIComponent(kiroManagementMethod)}`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
// Call CLIProxyAPI to start OAuth and get auth URL
|
// Call CLIProxyAPI to start OAuth and get auth URL
|
||||||
|
|||||||
@@ -20,8 +20,18 @@ describe('auth-types paste-callback start path', () => {
|
|||||||
expect(getPasteCallbackStartPath('ghcp')).toBe('/v0/management/github-auth-url?is_webui=true');
|
expect(getPasteCallbackStartPath('ghcp')).toBe('/v0/management/github-auth-url?is_webui=true');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps Kiro on the legacy start route for paste-callback mode', () => {
|
it('maps Kiro management-supported methods to the management auth-url route', () => {
|
||||||
expect(getPasteCallbackStartPath('kiro')).toBe('/oauth/kiro/start');
|
expect(getPasteCallbackStartPath('kiro')).toBe(
|
||||||
|
'/v0/management/kiro-auth-url?is_webui=true&method=aws'
|
||||||
|
);
|
||||||
|
expect(getPasteCallbackStartPath('kiro', { kiroMethod: 'google' })).toBe(
|
||||||
|
'/v0/management/kiro-auth-url?is_webui=true&method=google'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null for Kiro CLI-only paste-callback modes', () => {
|
||||||
|
expect(getPasteCallbackStartPath('kiro', { kiroMethod: 'aws-authcode' })).toBeNull();
|
||||||
|
expect(getPasteCallbackStartPath('kiro', { kiroMethod: 'idc' })).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('still exposes the generic management auth-url helper', () => {
|
it('still exposes the generic management auth-url helper', () => {
|
||||||
|
|||||||
@@ -39,11 +39,10 @@ describe('requestPasteCallbackStart', () => {
|
|||||||
expect(request.headers['Content-Type']).toBeUndefined();
|
expect(request.headers['Content-Type']).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps kiro on the legacy start route with POST', async () => {
|
it('uses the Kiro management auth-url route for paste-callback compatible methods', async () => {
|
||||||
mockFetch([
|
mockFetch([
|
||||||
{
|
{
|
||||||
url: /\/oauth\/kiro\/start$/,
|
url: /\/v0\/management\/kiro-auth-url\?is_webui=true&method=aws$/,
|
||||||
method: 'POST',
|
|
||||||
response: { auth_url: 'https://auth.example.com/kiro' },
|
response: { auth_url: 'https://auth.example.com/kiro' },
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -51,15 +50,29 @@ describe('requestPasteCallbackStart', () => {
|
|||||||
const { requestPasteCallbackStart } = await import(
|
const { requestPasteCallbackStart } = await import(
|
||||||
`../../../src/cliproxy/auth/oauth-handler?request-kiro-start=${Date.now()}`
|
`../../../src/cliproxy/auth/oauth-handler?request-kiro-start=${Date.now()}`
|
||||||
);
|
);
|
||||||
const startData = await requestPasteCallbackStart('kiro', remoteTarget);
|
const startData = await requestPasteCallbackStart('kiro', remoteTarget, {
|
||||||
|
kiroMethod: 'aws',
|
||||||
|
});
|
||||||
|
|
||||||
expect(startData.auth_url).toBe('https://auth.example.com/kiro');
|
expect(startData.auth_url).toBe('https://auth.example.com/kiro');
|
||||||
|
|
||||||
const [request] = getCapturedFetchRequests();
|
const [request] = getCapturedFetchRequests();
|
||||||
expect(request.url).toBe('https://proxy.example.com:8317/oauth/kiro/start');
|
expect(request.url).toBe(
|
||||||
expect(request.method).toBe('POST');
|
'https://proxy.example.com:8317/v0/management/kiro-auth-url?is_webui=true&method=aws'
|
||||||
|
);
|
||||||
|
expect(request.method).toBe('GET');
|
||||||
expect(request.headers['Authorization']).toBe('Bearer test-mgmt-key');
|
expect(request.headers['Authorization']).toBe('Bearer test-mgmt-key');
|
||||||
expect(request.headers['Content-Type']).toBe('application/json');
|
expect(request.headers['Content-Type']).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws for Kiro methods that require the local callback server flow', async () => {
|
||||||
|
const { requestPasteCallbackStart } = await import(
|
||||||
|
`../../../src/cliproxy/auth/oauth-handler?request-kiro-authcode-start=${Date.now()}`
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
requestPasteCallbackStart('kiro', remoteTarget, { kiroMethod: 'aws-authcode' })
|
||||||
|
).rejects.toThrow(/paste-callback start is not available/i);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ import {
|
|||||||
import {
|
import {
|
||||||
DEFAULT_KIRO_AUTH_METHOD,
|
DEFAULT_KIRO_AUTH_METHOD,
|
||||||
getKiroCallbackPort,
|
getKiroCallbackPort,
|
||||||
|
getKiroCLIAuthArgs,
|
||||||
getKiroCLIAuthFlag,
|
getKiroCLIAuthFlag,
|
||||||
|
normalizeKiroIDCFlow,
|
||||||
normalizeKiroAuthMethod,
|
normalizeKiroAuthMethod,
|
||||||
OAUTH_CALLBACK_PORTS as AUTH_CALLBACK_PORTS,
|
OAUTH_CALLBACK_PORTS as AUTH_CALLBACK_PORTS,
|
||||||
toKiroManagementMethod,
|
toKiroManagementMethod,
|
||||||
@@ -136,20 +138,30 @@ describe('provider-capabilities', () => {
|
|||||||
expect(DEFAULT_KIRO_AUTH_METHOD).toBe('aws');
|
expect(DEFAULT_KIRO_AUTH_METHOD).toBe('aws');
|
||||||
expect(normalizeKiroAuthMethod()).toBe('aws');
|
expect(normalizeKiroAuthMethod()).toBe('aws');
|
||||||
expect(normalizeKiroAuthMethod('GOOGLE')).toBe('google');
|
expect(normalizeKiroAuthMethod('GOOGLE')).toBe('google');
|
||||||
|
expect(normalizeKiroAuthMethod('IDC')).toBe('idc');
|
||||||
expect(normalizeKiroAuthMethod('not-valid')).toBe('aws');
|
expect(normalizeKiroAuthMethod('not-valid')).toBe('aws');
|
||||||
|
expect(normalizeKiroIDCFlow()).toBe('authcode');
|
||||||
|
expect(normalizeKiroIDCFlow('DEVICE')).toBe('device');
|
||||||
|
|
||||||
expect(getKiroCLIAuthFlag('aws')).toBe('--kiro-aws-login');
|
expect(getKiroCLIAuthFlag('aws')).toBe('--kiro-aws-login');
|
||||||
expect(getKiroCLIAuthFlag('aws-authcode')).toBe('--kiro-aws-authcode');
|
expect(getKiroCLIAuthFlag('aws-authcode')).toBe('--kiro-aws-authcode');
|
||||||
expect(getKiroCLIAuthFlag('google')).toBe('--kiro-google-login');
|
expect(getKiroCLIAuthFlag('google')).toBe('--kiro-google-login');
|
||||||
|
expect(getKiroCLIAuthFlag('idc')).toBe('--kiro-idc-login');
|
||||||
|
expect(getKiroCLIAuthArgs('idc', { idcStartUrl: 'https://d-123.awsapps.com/start' })).toEqual(
|
||||||
|
['--kiro-idc-login', '--kiro-idc-start-url', 'https://d-123.awsapps.com/start', '--kiro-idc-flow', 'authcode']
|
||||||
|
);
|
||||||
|
|
||||||
expect(getKiroCallbackPort('aws')).toBeNull();
|
expect(getKiroCallbackPort('aws')).toBeNull();
|
||||||
expect(getKiroCallbackPort('google')).toBe(9876);
|
expect(getKiroCallbackPort('google')).toBe(9876);
|
||||||
expect(getKiroCallbackPort('github')).toBe(9876);
|
expect(getKiroCallbackPort('github')).toBe(9876);
|
||||||
expect(getKiroCallbackPort('aws-authcode')).toBe(9876);
|
expect(getKiroCallbackPort('aws-authcode')).toBe(9876);
|
||||||
|
expect(getKiroCallbackPort('idc')).toBe(9876);
|
||||||
|
expect(getKiroCallbackPort('idc', { idcFlow: 'device' })).toBeNull();
|
||||||
|
|
||||||
expect(toKiroManagementMethod('aws')).toBe('aws');
|
expect(toKiroManagementMethod('aws')).toBe('aws');
|
||||||
expect(toKiroManagementMethod('aws-authcode')).toBe('aws');
|
expect(toKiroManagementMethod('aws-authcode')).toBe('aws');
|
||||||
expect(toKiroManagementMethod('google')).toBe('google');
|
expect(toKiroManagementMethod('google')).toBe('google');
|
||||||
expect(toKiroManagementMethod('github')).toBe('github');
|
expect(toKiroManagementMethod('github')).toBe('github');
|
||||||
|
expect(toKiroManagementMethod('idc')).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,6 +25,12 @@ describe('cliproxy-auth-routes start-url guard', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('rejects Kiro idc method on start-url', () => {
|
||||||
|
expect(getStartUrlUnsupportedReason('kiro', { kiroMethod: 'idc' })).toContain(
|
||||||
|
"Kiro method 'idc' uses CLI auth flow"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('allows authorization code providers', () => {
|
it('allows authorization code providers', () => {
|
||||||
expect(getStartUrlUnsupportedReason('gemini')).toBeNull();
|
expect(getStartUrlUnsupportedReason('gemini')).toBeNull();
|
||||||
expect(getStartUrlUnsupportedReason('codex')).toBeNull();
|
expect(getStartUrlUnsupportedReason('codex')).toBeNull();
|
||||||
|
|||||||
Reference in New Issue
Block a user