From 826a42d8d6c4877431474c27e14f0f1d4ec092d7 Mon Sep 17 00:00:00 2001
From: Tam Nhu Tran
Date: Sat, 14 Feb 2026 10:55:14 +0700
Subject: [PATCH 1/7] fix(cliproxy): harden kiro device-code auth flow
consistency
---
src/cliproxy/auth/auth-types.ts | 4 +-
src/management/oauth-port-diagnostics.ts | 52 ++++++++++---------
src/web-server/routes/cliproxy-auth-routes.ts | 13 +++++
.../cliproxy/provider-capabilities.test.ts | 19 +++++++
.../web-server/cliproxy-auth-routes.test.ts | 16 ++++++
.../components/account/add-account-dialog.tsx | 48 +++++++++++------
ui/src/hooks/use-cliproxy-auth-flow.ts | 2 +-
ui/src/hooks/use-device-code.ts | 3 +-
ui/src/lib/provider-config.ts | 8 +++
9 files changed, 122 insertions(+), 43 deletions(-)
create mode 100644 tests/unit/web-server/cliproxy-auth-routes.test.ts
diff --git a/src/cliproxy/auth/auth-types.ts b/src/cliproxy/auth/auth-types.ts
index 234a00ba..1f66114f 100644
--- a/src/cliproxy/auth/auth-types.ts
+++ b/src/cliproxy/auth/auth-types.ts
@@ -15,19 +15,19 @@ import { AccountInfo } from '../account-manager';
* - Gemini: Authorization Code Flow with local callback server on port 8085
* - Codex: Authorization Code Flow with local callback server on port 1455
* - Agy: Authorization Code Flow with local callback server on port 51121
- * - Kiro: Authorization Code Flow with local callback server on port 9876
* - iFlow: Authorization Code Flow with local callback server on port 11451
* - Claude: Authorization Code Flow with local callback server on port 54545 (Anthropic OAuth)
+ * - Kiro: Device Code Flow (polling-based, NO callback port needed)
* - Qwen: Device Code Flow (polling-based, NO callback port needed)
* - GHCP: Device Code Flow (polling-based, NO callback port needed)
*/
export const OAUTH_CALLBACK_PORTS: Partial> = {
gemini: 8085,
- kiro: 9876,
codex: 1455,
agy: 51121,
iflow: 11451,
claude: 54545,
+ // kiro: Device Code Flow - no callback port
// qwen: Device Code Flow - no callback port
// ghcp: Device Code Flow - no callback port
};
diff --git a/src/management/oauth-port-diagnostics.ts b/src/management/oauth-port-diagnostics.ts
index d50c16cf..8a13628d 100644
--- a/src/management/oauth-port-diagnostics.ts
+++ b/src/management/oauth-port-diagnostics.ts
@@ -9,7 +9,7 @@
* - Codex: 1455
* - Agy: 51121
* - iFlow: 11451
- * - Kiro: 9876
+ * - Kiro: Device Code Flow (no port needed)
* - Claude: 54545
* - Qwen: Device Code Flow (no port needed)
* - GHCP: Device Code Flow (no port needed)
@@ -26,40 +26,44 @@ import {
} from '../utils/port-utils';
import { CLIProxyProvider } from '../cliproxy/types';
import { CLIPROXY_PROFILES } from '../auth/profile-detector';
+import {
+ CLIPROXY_PROVIDER_IDS,
+ getOAuthCallbackPort,
+ getOAuthFlowType,
+ type OAuthFlowType as ProviderOAuthFlowType,
+} from '../cliproxy/provider-capabilities';
/**
- * OAuth callback ports for each provider
- * Extracted from CLIProxyAPI source
+ * Build provider-indexed records from canonical provider capabilities.
+ * Keeps diagnostics in sync with runtime OAuth flow metadata.
*/
-export const OAUTH_CALLBACK_PORTS: Record = {
- gemini: 8085,
- codex: 1455,
- agy: 51121,
- qwen: null, // Device Code Flow - no callback port
- iflow: 11451, // Authorization Code Flow
- kiro: 9876, // Authorization Code Flow
- ghcp: null, // Device Code Flow - no callback port
- claude: 54545, // Authorization Code Flow (Anthropic OAuth)
-};
+function buildProviderMap(
+ valueFor: (provider: CLIProxyProvider) => T
+): Record {
+ return CLIPROXY_PROVIDER_IDS.reduce(
+ (acc, provider) => {
+ acc[provider] = valueFor(provider);
+ return acc;
+ },
+ {} as Record
+ );
+}
+
+export const OAUTH_CALLBACK_PORTS: Record = buildProviderMap(
+ (provider) => getOAuthCallbackPort(provider)
+);
/**
* OAuth flow types
*/
-export type OAuthFlowType = 'authorization_code' | 'device_code';
+export type OAuthFlowType = ProviderOAuthFlowType;
/**
* OAuth flow type per provider
*/
-export const OAUTH_FLOW_TYPES: Record = {
- gemini: 'authorization_code',
- codex: 'authorization_code',
- agy: 'authorization_code',
- qwen: 'device_code',
- iflow: 'authorization_code',
- kiro: 'authorization_code',
- ghcp: 'device_code',
- claude: 'authorization_code',
-};
+export const OAUTH_FLOW_TYPES: Record = buildProviderMap(
+ (provider) => getOAuthFlowType(provider)
+);
/**
* Port diagnostic result
diff --git a/src/web-server/routes/cliproxy-auth-routes.ts b/src/web-server/routes/cliproxy-auth-routes.ts
index 302323ba..0930e873 100644
--- a/src/web-server/routes/cliproxy-auth-routes.ts
+++ b/src/web-server/routes/cliproxy-auth-routes.ts
@@ -38,6 +38,7 @@ import {
CLIPROXY_CALLBACK_PROVIDER_MAP,
CLIPROXY_AUTH_URL_PROVIDER_MAP,
} from '../../cliproxy/auth/auth-types';
+import { getOAuthFlowType } from '../../cliproxy/provider-capabilities';
import type { CLIProxyProvider } from '../../cliproxy/types';
import { CLIPROXY_PROFILES } from '../../auth/profile-detector';
@@ -46,6 +47,13 @@ const router = Router();
// Valid providers list - derived from canonical CLIPROXY_PROFILES
const validProviders: CLIProxyProvider[] = [...CLIPROXY_PROFILES];
+export function getStartUrlUnsupportedReason(provider: CLIProxyProvider): string | null {
+ if (getOAuthFlowType(provider) === 'device_code') {
+ return `Provider '${provider}' uses Device Code flow. Use /api/cliproxy/auth/${provider}/start instead.`;
+ }
+ return null;
+}
+
/**
* GET /api/cliproxy/auth - Get auth status for built-in CLIProxy profiles
* Also fetches CLIProxyAPI stats to update lastUsedAt for active providers
@@ -557,6 +565,11 @@ router.post('/:provider/start-url', async (req: Request, res: Response): Promise
res.status(400).json({ error: `Invalid provider: ${provider}` });
return;
}
+ const unsupportedReason = getStartUrlUnsupportedReason(provider as CLIProxyProvider);
+ if (unsupportedReason) {
+ res.status(400).json({ error: unsupportedReason });
+ return;
+ }
try {
const authUrlProvider =
diff --git a/tests/unit/cliproxy/provider-capabilities.test.ts b/tests/unit/cliproxy/provider-capabilities.test.ts
index 7aa13e7e..3434c344 100644
--- a/tests/unit/cliproxy/provider-capabilities.test.ts
+++ b/tests/unit/cliproxy/provider-capabilities.test.ts
@@ -2,11 +2,17 @@ import { describe, expect, it } from 'bun:test';
import {
CLIPROXY_PROVIDER_IDS,
getOAuthCallbackPort,
+ getOAuthFlowType,
getProviderDisplayName,
getProvidersByOAuthFlow,
isCLIProxyProvider,
mapExternalProviderName,
} from '../../../src/cliproxy/provider-capabilities';
+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';
describe('provider-capabilities', () => {
it('keeps canonical provider IDs backward-compatible', () => {
@@ -56,4 +62,17 @@ describe('provider-capabilities', () => {
expect(getOAuthCallbackPort('gemini')).toBe(8085);
expect(getProviderDisplayName('agy')).toBe('AntiGravity');
});
+
+ it('keeps diagnostics flow metadata in sync with provider capabilities', () => {
+ for (const provider of CLIPROXY_PROVIDER_IDS) {
+ expect(OAUTH_FLOW_TYPES[provider]).toBe(getOAuthFlowType(provider));
+ expect(DIAGNOSTIC_CALLBACK_PORTS[provider]).toBe(getOAuthCallbackPort(provider));
+ }
+ });
+
+ it('does not define callback ports for device code providers in auth constants', () => {
+ for (const provider of getProvidersByOAuthFlow('device_code')) {
+ expect(AUTH_CALLBACK_PORTS[provider]).toBeUndefined();
+ }
+ });
});
diff --git a/tests/unit/web-server/cliproxy-auth-routes.test.ts b/tests/unit/web-server/cliproxy-auth-routes.test.ts
new file mode 100644
index 00000000..025c0e8b
--- /dev/null
+++ b/tests/unit/web-server/cliproxy-auth-routes.test.ts
@@ -0,0 +1,16 @@
+import { describe, expect, it } from 'bun:test';
+import { getStartUrlUnsupportedReason } from '../../../src/web-server/routes/cliproxy-auth-routes';
+
+describe('cliproxy-auth-routes start-url guard', () => {
+ it('rejects device code providers', () => {
+ expect(getStartUrlUnsupportedReason('kiro')).toContain("Provider 'kiro' uses Device Code flow");
+ expect(getStartUrlUnsupportedReason('ghcp')).toContain("Provider 'ghcp' uses Device Code flow");
+ expect(getStartUrlUnsupportedReason('qwen')).toContain("Provider 'qwen' uses Device Code flow");
+ });
+
+ it('allows authorization code providers', () => {
+ expect(getStartUrlUnsupportedReason('gemini')).toBeNull();
+ expect(getStartUrlUnsupportedReason('codex')).toBeNull();
+ expect(getStartUrlUnsupportedReason('claude')).toBeNull();
+ });
+});
diff --git a/ui/src/components/account/add-account-dialog.tsx b/ui/src/components/account/add-account-dialog.tsx
index 55ef8079..abae2ba8 100644
--- a/ui/src/components/account/add-account-dialog.tsx
+++ b/ui/src/components/account/add-account-dialog.tsx
@@ -1,7 +1,7 @@
/**
* Add Account Dialog Component
* Uses /start-url to get OAuth URL + polls for completion via management API.
- * For Device Code flows (ghcp, qwen): Uses /start endpoint which spawns CLIProxy
+ * For Device Code flows (ghcp, qwen, kiro): Uses /start endpoint which spawns CLIProxy
* binary and emits WebSocket events. DeviceCodeDialog handles user code display.
* Shows auth URL + callback paste field. Polling auto-closes on success.
* For Kiro: Also shows "Import from IDE" option.
@@ -22,7 +22,7 @@ import { Loader2, ExternalLink, User, Download, Copy, Check } from 'lucide-react
import { useKiroImport } from '@/hooks/use-cliproxy';
import { useCliproxyAuthFlow } from '@/hooks/use-cliproxy-auth-flow';
import { applyDefaultPreset } from '@/lib/preset-utils';
-import { isDeviceCodeProvider } from '@/lib/provider-config';
+import { isDeviceCodeProvider, isNicknameRequiredProvider } from '@/lib/provider-config';
import { toast } from 'sonner';
interface AddAccountDialogProps {
@@ -44,18 +44,23 @@ export function AddAccountDialog({
const [nickname, setNickname] = useState('');
const [callbackUrl, setCallbackUrl] = useState('');
const [copied, setCopied] = useState(false);
+ const [localError, setLocalError] = useState(null);
const wasAuthenticatingRef = useRef(false);
const authFlow = useCliproxyAuthFlow();
const kiroImportMutation = useKiroImport();
const isKiro = provider === 'kiro';
const isDeviceCode = isDeviceCodeProvider(provider);
+ const requiresNickname = isNicknameRequiredProvider(provider);
const isPending = authFlow.isAuthenticating || kiroImportMutation.isPending;
+ const nicknameTrimmed = nickname.trim();
+ const errorMessage = localError || authFlow.error;
const resetAndClose = () => {
setNickname('');
setCallbackUrl('');
setCopied(false);
+ setLocalError(null);
wasAuthenticatingRef.current = false;
onClose();
};
@@ -103,13 +108,18 @@ export function AddAccountDialog({
};
/**
- * Authenticate via /start-url + polling only.
- * Does NOT call /start (which spawns a local CLIProxy binary that kills running instances).
- * /start-url uses the management API to get auth URL, then polls for completion.
+ * Start auth flow using provider capabilities.
+ * - Device code providers use /start and rely on WebSocket events for code display.
+ * - Authorization code providers use /start-url and polling.
*/
const handleAuthenticate = () => {
+ if (requiresNickname && !nicknameTrimmed) {
+ setLocalError(`Nickname is required for ${displayName} accounts.`);
+ return;
+ }
+ setLocalError(null);
wasAuthenticatingRef.current = true;
- authFlow.startAuth(provider, { nickname: nickname.trim() || undefined });
+ authFlow.startAuth(provider, { nickname: nicknameTrimmed || undefined });
};
const handleKiroImport = () => {
@@ -157,20 +167,27 @@ export function AddAccountDialog({
{/* Nickname input - only show before auth starts */}
{!showAuthUI && (
-
Nickname (optional)
+
+ {requiresNickname ? 'Nickname (required)' : 'Nickname (optional)'}
+
setNickname(e.target.value)}
+ onChange={(e) => {
+ setNickname(e.target.value);
+ setLocalError(null);
+ }}
placeholder="e.g., work, personal"
disabled={isPending}
className="flex-1"
/>
- A friendly name to identify this account. Auto-generated from email if left empty.
+ {requiresNickname
+ ? 'Required for this provider. Use a unique friendly name (e.g., work, personal).'
+ : 'A friendly name to identify this account. Auto-generated from email if left empty.'}
)}
@@ -191,11 +208,6 @@ export function AddAccountDialog({
- {/* Error display */}
- {authFlow.error && !authFlow.authUrl && (
- {authFlow.error}
- )}
-
{/* Auth URL section - only for Authorization Code flows, NOT Device Code */}
{authFlow.authUrl && !authFlow.isDeviceCodeFlow && (
@@ -273,6 +285,9 @@ export function AddAccountDialog({
)}
+ {/* Persist error visibility outside auth-only UI states */}
+ {errorMessage && {errorMessage}
}
+
{/* Kiro import loading */}
{kiroImportMutation.isPending && (
@@ -302,7 +317,10 @@ export function AddAccountDialog({
)}
{!showAuthUI && (
-
+
Authenticate
diff --git a/ui/src/hooks/use-cliproxy-auth-flow.ts b/ui/src/hooks/use-cliproxy-auth-flow.ts
index 433af6e2..73a1d5c4 100644
--- a/ui/src/hooks/use-cliproxy-auth-flow.ts
+++ b/ui/src/hooks/use-cliproxy-auth-flow.ts
@@ -19,7 +19,7 @@ interface AuthFlowState {
oauthState: string | null;
/** Whether callback is being submitted */
isSubmittingCallback: boolean;
- /** Whether this is a device code flow (ghcp, qwen) - dialog handled separately via WebSocket */
+ /** Whether this is a device code flow (ghcp, qwen, kiro) - dialog handled separately via WebSocket */
isDeviceCodeFlow: boolean;
}
diff --git a/ui/src/hooks/use-device-code.ts b/ui/src/hooks/use-device-code.ts
index a8492068..8c69bd77 100644
--- a/ui/src/hooks/use-device-code.ts
+++ b/ui/src/hooks/use-device-code.ts
@@ -3,7 +3,7 @@
*
* Listens for WebSocket device code events and manages dialog state.
* Similar to useProjectSelection but for Device Code OAuth flows
- * (GitHub Copilot, Qwen, etc.)
+ * (GitHub Copilot, Qwen, Kiro, etc.)
*/
import { useState, useEffect, useCallback, useMemo } from 'react';
@@ -26,6 +26,7 @@ interface DeviceCodeState {
/** Provider display names for user-friendly messages */
const PROVIDER_DISPLAY_NAMES: Record = {
ghcp: 'GitHub Copilot',
+ kiro: 'Kiro (AWS)',
qwen: 'Qwen Code',
};
diff --git a/ui/src/lib/provider-config.ts b/ui/src/lib/provider-config.ts
index bdcbd378..2da393ba 100644
--- a/ui/src/lib/provider-config.ts
+++ b/ui/src/lib/provider-config.ts
@@ -82,3 +82,11 @@ export const DEVICE_CODE_PROVIDERS: CLIProxyProvider[] = ['ghcp', 'kiro', 'qwen'
export function isDeviceCodeProvider(provider: string): boolean {
return DEVICE_CODE_PROVIDERS.includes(provider as CLIProxyProvider);
}
+
+/** Providers that require nickname because token payload may not include email. */
+export const NICKNAME_REQUIRED_PROVIDERS: CLIProxyProvider[] = ['ghcp', 'kiro'];
+
+/** Check if provider requires user-supplied nickname in auth flow */
+export function isNicknameRequiredProvider(provider: string): boolean {
+ return NICKNAME_REQUIRED_PROVIDERS.includes(provider as CLIProxyProvider);
+}
From fd46ad022aae1a4d2fe6dbb8304f691465860214 Mon Sep 17 00:00:00 2001
From: Tam Nhu Tran
Date: Sat, 14 Feb 2026 13:33:28 +0700
Subject: [PATCH 2/7] 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
---
src/cliproxy/auth/auth-types.ts | 74 ++++++++++++++++++-
src/cliproxy/auth/oauth-handler.ts | 35 +++++++--
.../cliproxy/provider-capabilities.test.ts | 30 +++++++-
3 files changed, 132 insertions(+), 7 deletions(-)
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');
+ });
});
From e425f477a44fa030a8dc5a6c28bed14d9bdfd762 Mon Sep 17 00:00:00 2001
From: Tam Nhu Tran
Date: Sat, 14 Feb 2026 13:33:37 +0700
Subject: [PATCH 3/7] fix(cli): add kiro auth-method flag wiring and help
- parse and validate --kiro-auth-method for Kiro and Kiro composite tiers
- pass selected method through auth/import flows
- document supported Kiro auth-method values in ccs --help
Refs #552
Refs #233
---
src/cliproxy/executor/index.ts | 33 +++++++++++++++++++++++++++++++++
src/commands/help-command.ts | 4 ++++
2 files changed, 37 insertions(+)
diff --git a/src/cliproxy/executor/index.ts b/src/cliproxy/executor/index.ts
index c084dc68..3ea5b1c7 100644
--- a/src/cliproxy/executor/index.ts
+++ b/src/cliproxy/executor/index.ts
@@ -52,6 +52,7 @@ import {
import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
import { installImageAnalyzerHook } from '../../utils/hooks';
import { HttpsTunnelProxy } from '../https-tunnel-proxy';
+import { isKiroAuthMethod, KiroAuthMethod, normalizeKiroAuthMethod } from '../auth/auth-types';
// Import modular components
import { waitForProxyReadyWithSpinner, spawnProxy } from './lifecycle-manager';
@@ -307,6 +308,33 @@ export async function execClaudeWithCLIProxy(
setNickname = argsWithoutProxy[nicknameIdx + 1];
}
+ // Parse --kiro-auth-method flag
+ let kiroAuthMethod: KiroAuthMethod | undefined;
+ const kiroMethodIdx = argsWithoutProxy.indexOf('--kiro-auth-method');
+ if (kiroMethodIdx !== -1) {
+ const rawMethod = argsWithoutProxy[kiroMethodIdx + 1];
+ if (!rawMethod || rawMethod.startsWith('-')) {
+ console.error(fail('--kiro-auth-method requires a value'));
+ console.error(' Supported values: aws, aws-authcode, google, github');
+ process.exitCode = 1;
+ return;
+ }
+ const normalized = rawMethod.trim().toLowerCase();
+ if (!isKiroAuthMethod(normalized)) {
+ console.error(fail(`Invalid --kiro-auth-method value: ${rawMethod}`));
+ console.error(' Supported values: aws, aws-authcode, google, github');
+ process.exitCode = 1;
+ return;
+ }
+ kiroAuthMethod = normalizeKiroAuthMethod(normalized);
+ }
+
+ if (kiroAuthMethod && provider !== 'kiro' && !compositeProviders.includes('kiro')) {
+ console.error(fail('--kiro-auth-method is only valid for ccs kiro'));
+ process.exitCode = 1;
+ return;
+ }
+
// Parse --thinking / --effort flags (aliases; first occurrence wins)
const thinkingParse = parseThinkingOverride(argsWithoutProxy);
if (thinkingParse.error) {
@@ -465,6 +493,7 @@ export async function execClaudeWithCLIProxy(
const authSuccess = await triggerOAuth(provider, {
verbose,
import: true,
+ ...(kiroAuthMethod ? { kiroMethod: kiroAuthMethod } : {}),
...(setNickname ? { nickname: setNickname } : {}),
});
if (!authSuccess) {
@@ -495,6 +524,7 @@ export async function execClaudeWithCLIProxy(
const authSuccess = await triggerOAuth(p, {
verbose,
add: addAccount,
+ ...(kiroAuthMethod && p === 'kiro' ? { kiroMethod: kiroAuthMethod } : {}),
...(forceHeadless ? { headless: true } : {}),
...(setNickname ? { nickname: setNickname } : {}),
...(noIncognito ? { noIncognito: true } : {}),
@@ -535,6 +565,7 @@ export async function execClaudeWithCLIProxy(
const authSuccess = await triggerOAuth(provider, {
verbose,
add: addAccount,
+ ...(kiroAuthMethod ? { kiroMethod: kiroAuthMethod } : {}),
...(forceHeadless ? { headless: true } : {}),
...(setNickname ? { nickname: setNickname } : {}),
...(noIncognito ? { noIncognito: true } : {}),
@@ -854,6 +885,7 @@ export async function execClaudeWithCLIProxy(
'--accounts',
'--use',
'--nickname',
+ '--kiro-auth-method',
'--thinking',
'--effort',
'--1m',
@@ -872,6 +904,7 @@ export async function execClaudeWithCLIProxy(
if (
argsWithoutProxy[idx - 1] === '--use' ||
argsWithoutProxy[idx - 1] === '--nickname' ||
+ argsWithoutProxy[idx - 1] === '--kiro-auth-method' ||
argsWithoutProxy[idx - 1] === '--thinking' ||
argsWithoutProxy[idx - 1] === '--effort'
)
diff --git a/src/commands/help-command.ts b/src/commands/help-command.ts
index 0e8f0065..5d269d75 100644
--- a/src/commands/help-command.ts
+++ b/src/commands/help-command.ts
@@ -193,6 +193,10 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
['ccs --logout', 'Clear authentication'],
['ccs --headless', 'Headless auth (for SSH)'],
['ccs --port-forward', 'Force port-forwarding mode (skip prompt)'],
+ ['ccs kiro --auth --kiro-auth-method aws', 'Kiro via AWS Builder ID (device code)'],
+ ['ccs kiro --auth --kiro-auth-method aws-authcode', 'Kiro via AWS auth code flow'],
+ ['ccs kiro --auth --kiro-auth-method google', 'Kiro via Google OAuth'],
+ ['ccs kiro --auth --kiro-auth-method github', 'Kiro via GitHub OAuth (Dashboard flow)'],
['ccs kiro --import', 'Import token from Kiro IDE'],
['ccs kiro --incognito', 'Use incognito browser (default: normal)'],
['ccs codex "explain code"', 'Use with prompt'],
From 577991c282fb3646f42d5c657c94dd5c07f6b4e3 Mon Sep 17 00:00:00 2001
From: Tam Nhu Tran
Date: Sat, 14 Feb 2026 13:33:48 +0700
Subject: [PATCH 4/7] fix(web-server): make kiro auth routes method-aware
- validate kiroMethod on /start and /start-url with clear errors
- route Kiro management start-url requests with upstream method query
- accept state-first responses and keep auth_url optional
- add start-url guard tests for Kiro social and aws-authcode paths
Refs #552
Refs #233
---
src/web-server/routes/cliproxy-auth-routes.ts | 94 +++++++++++++++++--
.../web-server/cliproxy-auth-routes.test.ts | 13 ++-
2 files changed, 97 insertions(+), 10 deletions(-)
diff --git a/src/web-server/routes/cliproxy-auth-routes.ts b/src/web-server/routes/cliproxy-auth-routes.ts
index 0930e873..a940a117 100644
--- a/src/web-server/routes/cliproxy-auth-routes.ts
+++ b/src/web-server/routes/cliproxy-auth-routes.ts
@@ -37,6 +37,11 @@ import { getProviderTokenDir } from '../../cliproxy/auth/token-manager';
import {
CLIPROXY_CALLBACK_PROVIDER_MAP,
CLIPROXY_AUTH_URL_PROVIDER_MAP,
+ isKiroAuthMethod,
+ isKiroDeviceCodeMethod,
+ KiroAuthMethod,
+ normalizeKiroAuthMethod,
+ toKiroManagementMethod,
} from '../../cliproxy/auth/auth-types';
import { getOAuthFlowType } from '../../cliproxy/provider-capabilities';
import type { CLIProxyProvider } from '../../cliproxy/types';
@@ -47,7 +52,38 @@ const router = Router();
// Valid providers list - derived from canonical CLIPROXY_PROFILES
const validProviders: CLIProxyProvider[] = [...CLIPROXY_PROFILES];
-export function getStartUrlUnsupportedReason(provider: CLIProxyProvider): string | null {
+function parseKiroMethod(raw: unknown): { method: KiroAuthMethod; invalid: boolean } {
+ if (raw === undefined || raw === null) {
+ return { method: normalizeKiroAuthMethod(), invalid: false };
+ }
+ if (typeof raw !== 'string') {
+ return { method: normalizeKiroAuthMethod(), invalid: true };
+ }
+ if (raw.trim() === '') {
+ return { method: normalizeKiroAuthMethod(), invalid: false };
+ }
+ const normalized = raw.trim().toLowerCase();
+ if (!isKiroAuthMethod(normalized)) {
+ return { method: normalizeKiroAuthMethod(), invalid: true };
+ }
+ return { method: normalizeKiroAuthMethod(normalized), invalid: false };
+}
+
+export function getStartUrlUnsupportedReason(
+ provider: CLIProxyProvider,
+ options?: { kiroMethod?: KiroAuthMethod }
+): string | null {
+ if (provider === 'kiro') {
+ const kiroMethod = options?.kiroMethod ?? normalizeKiroAuthMethod();
+ if (kiroMethod === 'aws-authcode') {
+ return "Kiro method 'aws-authcode' uses CLI auth flow. Use /api/cliproxy/auth/kiro/start instead.";
+ }
+ if (isKiroDeviceCodeMethod(kiroMethod)) {
+ return "Kiro method 'aws' uses Device Code flow. Use /api/cliproxy/auth/kiro/start instead.";
+ }
+ return null;
+ }
+
if (getOAuthFlowType(provider) === 'device_code') {
return `Provider '${provider}' uses Device Code flow. Use /api/cliproxy/auth/${provider}/start instead.`;
}
@@ -351,9 +387,14 @@ router.post('/accounts/:provider/:accountId/resume', (req: Request, res: Respons
*/
router.post('/:provider/start', async (req: Request, res: Response): Promise => {
const { provider } = req.params;
- const { nickname: nicknameRaw, noIncognito: noIncognitoBody } = req.body;
+ const {
+ nickname: nicknameRaw,
+ noIncognito: noIncognitoBody,
+ kiroMethod: kiroMethodRaw,
+ } = req.body;
// Trim nickname for consistency with CLI (oauth-handler.ts trims input)
const nickname = typeof nicknameRaw === 'string' ? nicknameRaw.trim() : nicknameRaw;
+ const { method: kiroMethod, invalid: invalidKiroMethod } = parseKiroMethod(kiroMethodRaw);
// Validate provider
if (!validProviders.includes(provider as CLIProxyProvider)) {
@@ -361,6 +402,14 @@ router.post('/:provider/start', async (req: Request, res: Response): Promise
*/
router.post('/:provider/start-url', async (req: Request, res: Response): Promise => {
const { provider } = req.params;
+ const { kiroMethod: kiroMethodRaw } = req.body ?? {};
+ const { method: kiroMethod, invalid: invalidKiroMethod } = parseKiroMethod(kiroMethodRaw);
// Check remote mode
const target = getProxyTarget();
@@ -565,7 +617,18 @@ router.post('/:provider/start-url', async (req: Request, res: Response): Promise
res.status(400).json({ error: `Invalid provider: ${provider}` });
return;
}
- const unsupportedReason = getStartUrlUnsupportedReason(provider as CLIProxyProvider);
+
+ if (provider === 'kiro' && invalidKiroMethod) {
+ res.status(400).json({
+ error: 'Invalid kiroMethod. Supported: aws, aws-authcode, google, github',
+ code: 'INVALID_KIRO_METHOD',
+ });
+ return;
+ }
+
+ const unsupportedReason = getStartUrlUnsupportedReason(provider as CLIProxyProvider, {
+ kiroMethod: provider === 'kiro' ? kiroMethod : undefined,
+ });
if (unsupportedReason) {
res.status(400).json({ error: unsupportedReason });
return;
@@ -574,11 +637,15 @@ router.post('/:provider/start-url', async (req: Request, res: Response): Promise
try {
const authUrlProvider =
CLIPROXY_AUTH_URL_PROVIDER_MAP[provider as CLIProxyProvider] || provider;
+ const kiroQuery =
+ provider === 'kiro'
+ ? `&method=${encodeURIComponent(toKiroManagementMethod(kiroMethod))}`
+ : '';
// Call CLIProxyAPI to start OAuth and get auth URL
// CLIProxyAPI management routes are under /v0/management prefix
const response = await fetch(
- buildProxyUrl(target, `/v0/management/${authUrlProvider}-auth-url?is_webui=true`),
+ buildProxyUrl(target, `/v0/management/${authUrlProvider}-auth-url?is_webui=true${kiroQuery}`),
{ headers: buildManagementHeaders(target) }
);
@@ -588,18 +655,27 @@ router.post('/:provider/start-url', async (req: Request, res: Response): Promise
return;
}
- const data = (await response.json()) as { url?: string; auth_url?: string; state?: string };
+ const data = (await response.json()) as {
+ url?: string;
+ auth_url?: string;
+ state?: string;
+ method?: string;
+ };
const authUrl = data.url || data.auth_url;
- if (!authUrl) {
- res.status(500).json({ error: 'No authorization URL received from CLIProxyAPI' });
+ // Some upstream flows return state first and provide auth_url in subsequent status polling.
+ if (!authUrl && !data.state) {
+ res
+ .status(500)
+ .json({ error: 'No OAuth state or authorization URL received from CLIProxyAPI' });
return;
}
res.json({
success: true,
- authUrl,
- state: data.state,
+ authUrl: authUrl || null,
+ state: data.state || null,
+ method: data.method || null,
});
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to start OAuth';
diff --git a/tests/unit/web-server/cliproxy-auth-routes.test.ts b/tests/unit/web-server/cliproxy-auth-routes.test.ts
index 025c0e8b..1ad73d09 100644
--- a/tests/unit/web-server/cliproxy-auth-routes.test.ts
+++ b/tests/unit/web-server/cliproxy-auth-routes.test.ts
@@ -3,11 +3,22 @@ import { getStartUrlUnsupportedReason } from '../../../src/web-server/routes/cli
describe('cliproxy-auth-routes start-url guard', () => {
it('rejects device code providers', () => {
- expect(getStartUrlUnsupportedReason('kiro')).toContain("Provider 'kiro' uses Device Code flow");
+ expect(getStartUrlUnsupportedReason('kiro')).toContain("Kiro method 'aws' uses Device Code flow");
expect(getStartUrlUnsupportedReason('ghcp')).toContain("Provider 'ghcp' uses Device Code flow");
expect(getStartUrlUnsupportedReason('qwen')).toContain("Provider 'qwen' uses Device Code flow");
});
+ it('allows Kiro social methods on start-url', () => {
+ expect(getStartUrlUnsupportedReason('kiro', { kiroMethod: 'google' })).toBeNull();
+ expect(getStartUrlUnsupportedReason('kiro', { kiroMethod: 'github' })).toBeNull();
+ });
+
+ it('rejects Kiro aws-authcode method on start-url', () => {
+ expect(getStartUrlUnsupportedReason('kiro', { kiroMethod: 'aws-authcode' })).toContain(
+ "Kiro method 'aws-authcode' uses CLI auth flow"
+ );
+ });
+
it('allows authorization code providers', () => {
expect(getStartUrlUnsupportedReason('gemini')).toBeNull();
expect(getStartUrlUnsupportedReason('codex')).toBeNull();
From 6a21f3921b618ba61949cf1eeccc2251dd3f6719 Mon Sep 17 00:00:00 2001
From: Tam Nhu Tran
Date: Sat, 14 Feb 2026 13:33:57 +0700
Subject: [PATCH 5/7] fix(ui): add method-aware kiro auth flow parity
- add Kiro auth-method selector with AWS device-code default
- route start requests by method-specific flow and endpoint
- support auth_url status polling and delayed URL UX state
Refs #552
Refs #233
---
.../components/account/add-account-dialog.tsx | 63 +++++++++++++++-
ui/src/hooks/use-cliproxy-auth-flow.ts | 73 ++++++++++++++++---
ui/src/lib/provider-config.ts | 64 ++++++++++++++++
3 files changed, 184 insertions(+), 16 deletions(-)
diff --git a/ui/src/components/account/add-account-dialog.tsx b/ui/src/components/account/add-account-dialog.tsx
index abae2ba8..eeb6df55 100644
--- a/ui/src/components/account/add-account-dialog.tsx
+++ b/ui/src/components/account/add-account-dialog.tsx
@@ -18,11 +18,25 @@ import {
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
import { Loader2, ExternalLink, User, Download, Copy, Check } from 'lucide-react';
import { useKiroImport } from '@/hooks/use-cliproxy';
import { useCliproxyAuthFlow } from '@/hooks/use-cliproxy-auth-flow';
import { applyDefaultPreset } from '@/lib/preset-utils';
-import { isDeviceCodeProvider, isNicknameRequiredProvider } from '@/lib/provider-config';
+import {
+ DEFAULT_KIRO_AUTH_METHOD,
+ getKiroAuthMethodOption,
+ isDeviceCodeProvider,
+ isNicknameRequiredProvider,
+ KIRO_AUTH_METHOD_OPTIONS,
+ KiroAuthMethod,
+} from '@/lib/provider-config';
import { toast } from 'sonner';
interface AddAccountDialogProps {
@@ -45,13 +59,16 @@ export function AddAccountDialog({
const [callbackUrl, setCallbackUrl] = useState('');
const [copied, setCopied] = useState(false);
const [localError, setLocalError] = useState(null);
+ const [kiroAuthMethod, setKiroAuthMethod] = useState(DEFAULT_KIRO_AUTH_METHOD);
const wasAuthenticatingRef = useRef(false);
const authFlow = useCliproxyAuthFlow();
const kiroImportMutation = useKiroImport();
const isKiro = provider === 'kiro';
- const isDeviceCode = isDeviceCodeProvider(provider);
+ const defaultDeviceCode = isDeviceCodeProvider(provider);
const requiresNickname = isNicknameRequiredProvider(provider);
+ const kiroMethodOption = getKiroAuthMethodOption(kiroAuthMethod);
+ const isDeviceCode = isKiro ? kiroMethodOption.flowType === 'device_code' : defaultDeviceCode;
const isPending = authFlow.isAuthenticating || kiroImportMutation.isPending;
const nicknameTrimmed = nickname.trim();
const errorMessage = localError || authFlow.error;
@@ -61,6 +78,7 @@ export function AddAccountDialog({
setCallbackUrl('');
setCopied(false);
setLocalError(null);
+ setKiroAuthMethod(DEFAULT_KIRO_AUTH_METHOD);
wasAuthenticatingRef.current = false;
onClose();
};
@@ -119,7 +137,12 @@ export function AddAccountDialog({
}
setLocalError(null);
wasAuthenticatingRef.current = true;
- authFlow.startAuth(provider, { nickname: nicknameTrimmed || undefined });
+ authFlow.startAuth(provider, {
+ nickname: nicknameTrimmed || undefined,
+ kiroMethod: isKiro ? kiroAuthMethod : undefined,
+ flowType: isKiro ? kiroMethodOption.flowType : undefined,
+ startEndpoint: isKiro ? kiroMethodOption.startEndpoint : undefined,
+ });
};
const handleKiroImport = () => {
@@ -156,7 +179,7 @@ export function AddAccountDialog({
Add {displayName} Account
{isKiro
- ? 'Authenticate via browser or import an existing token from Kiro IDE.'
+ ? 'Choose a Kiro auth method, then authenticate via browser or import from Kiro IDE.'
: isDeviceCode
? 'Click Authenticate. A verification code will appear for you to enter on the provider website.'
: 'Click Authenticate to get an OAuth URL. Open it in any browser to sign in.'}
@@ -164,6 +187,32 @@ export function AddAccountDialog({
+ {/* Kiro auth method */}
+ {isKiro && !showAuthUI && (
+
+
Auth Method
+
{
+ setKiroAuthMethod(value as KiroAuthMethod);
+ setLocalError(null);
+ }}
+ >
+
+
+
+
+ {KIRO_AUTH_METHOD_OPTIONS.map((option) => (
+
+ {option.label}
+
+ ))}
+
+
+
{kiroMethodOption.description}
+
+ )}
+
{/* Nickname input - only show before auth starts */}
{!showAuthUI && (
@@ -282,6 +331,12 @@ export function AddAccountDialog({
)}
+
+ {!authFlow.authUrl && !authFlow.isDeviceCodeFlow && (
+
+ Preparing sign-in URL...
+
+ )}
)}
diff --git a/ui/src/hooks/use-cliproxy-auth-flow.ts b/ui/src/hooks/use-cliproxy-auth-flow.ts
index 73a1d5c4..a1f8f954 100644
--- a/ui/src/hooks/use-cliproxy-auth-flow.ts
+++ b/ui/src/hooks/use-cliproxy-auth-flow.ts
@@ -25,6 +25,9 @@ interface AuthFlowState {
interface StartAuthOptions {
nickname?: string;
+ kiroMethod?: string;
+ flowType?: 'authorization_code' | 'device_code';
+ startEndpoint?: 'start' | 'start-url';
}
/** Polling interval for OAuth status check (3 seconds) */
@@ -49,6 +52,7 @@ export function useCliproxyAuthFlow() {
const abortControllerRef = useRef(null);
const pollIntervalRef = useRef | null>(null);
const pollStartRef = useRef(0);
+ const openedAuthUrlRef = useRef(false);
const queryClient = useQueryClient();
// Clear polling
@@ -64,6 +68,7 @@ export function useCliproxyAuthFlow() {
return () => {
abortControllerRef.current?.abort();
stopPolling();
+ openedAuthUrlRef.current = false;
};
}, [stopPolling]);
@@ -85,14 +90,46 @@ export function useCliproxyAuthFlow() {
const response = await fetch(
`/api/cliproxy/auth/${provider}/status?state=${encodeURIComponent(oauthState)}`
);
- const data = await response.json();
+ const data = (await response.json()) as {
+ status?: string;
+ error?: string;
+ url?: string;
+ auth_url?: string;
+ verification_url?: string;
+ user_code?: string;
+ };
if (data.status === 'ok') {
stopPolling();
queryClient.invalidateQueries({ queryKey: ['cliproxy-auth'] });
queryClient.invalidateQueries({ queryKey: ['account-quota'] });
toast.success(`${provider} authentication successful`);
+ openedAuthUrlRef.current = false;
setState(INITIAL_STATE);
+ } else if (data.status === 'auth_url') {
+ const authUrl = data.url || data.auth_url;
+ if (authUrl) {
+ setState((prev) => ({
+ ...prev,
+ authUrl,
+ }));
+ if (!openedAuthUrlRef.current) {
+ openedAuthUrlRef.current = true;
+ window.open(authUrl, '_blank');
+ }
+ }
+ } else if (data.status === 'device_code') {
+ stopPolling();
+ const details =
+ data.user_code && data.verification_url
+ ? `Open ${data.verification_url} and enter code: ${data.user_code}`
+ : 'Switch to Device Code method and try again.';
+ toast.error('Provider returned Device Code flow in callback mode');
+ setState((prev) => ({
+ ...prev,
+ isAuthenticating: false,
+ error: details,
+ }));
} else if (data.status === 'error') {
stopPolling();
const errorMsg = data.error || 'Authentication failed';
@@ -103,7 +140,7 @@ export function useCliproxyAuthFlow() {
error: errorMsg,
}));
}
- // status === 'pending' means continue polling
+ // status === 'wait' (or pending) means continue polling
} catch {
// Network error - continue polling
}
@@ -124,12 +161,21 @@ export function useCliproxyAuthFlow() {
// Abort any in-progress auth
abortControllerRef.current?.abort();
stopPolling();
+ openedAuthUrlRef.current = false;
// Create fresh controller and capture locally to avoid race with cancelAuth
const controller = new AbortController();
abortControllerRef.current = controller;
- const deviceCodeFlow = isDeviceCodeProvider(provider);
+ const flowType =
+ options?.flowType ||
+ (isDeviceCodeProvider(provider) ? 'device_code' : 'authorization_code');
+ const deviceCodeFlow = flowType === 'device_code';
+ const startEndpoint = options?.startEndpoint || (deviceCodeFlow ? 'start' : 'start-url');
+ const payload = {
+ nickname: options?.nickname,
+ kiroMethod: options?.kiroMethod,
+ };
setState({
provider,
@@ -142,14 +188,13 @@ export function useCliproxyAuthFlow() {
});
try {
- if (deviceCodeFlow) {
- // Device Code Flow: Call /start endpoint which spawns CLIProxyAPI binary.
- // This emits WebSocket events with userCode that DeviceCodeDialog will display.
- // The /start endpoint blocks until completion, so we don't await it here.
+ if (startEndpoint === 'start') {
+ // /start spawns CLIProxy binary and blocks until completion.
+ // For Device Code flows, userCode is delivered via WebSocket.
fetch(`/api/cliproxy/auth/${provider}/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ nickname: options?.nickname }),
+ body: JSON.stringify(payload),
signal: controller.signal,
})
.then(async (response) => {
@@ -159,6 +204,7 @@ export function useCliproxyAuthFlow() {
queryClient.invalidateQueries({ queryKey: ['account-quota'] });
// Note: No toast here - DeviceCodeDialog's useDeviceCode hook handles success toast
// via deviceCodeCompleted WebSocket event to avoid duplicate toasts
+ openedAuthUrlRef.current = false;
setState(INITIAL_STATE);
} else {
const errorMsg = data.error || 'Authentication failed';
@@ -183,13 +229,13 @@ export function useCliproxyAuthFlow() {
error: message,
}));
});
- // Don't await - let the request run in background while DeviceCodeDialog handles UI
+ // Don't await - keeps UI responsive while backend auth is in progress
} else {
- // Authorization Code Flow: Call /start-url to get auth URL immediately (non-blocking)
+ // /start-url uses management API to bootstrap callback/social flows.
const response = await fetch(`/api/cliproxy/auth/${provider}/start-url`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ nickname: options?.nickname }),
+ body: JSON.stringify(payload),
signal: controller.signal,
});
@@ -202,12 +248,13 @@ export function useCliproxyAuthFlow() {
// Update state with auth URL
setState((prev) => ({
...prev,
- authUrl: data.authUrl,
+ authUrl: data.authUrl || null,
oauthState: data.state,
}));
// Auto-open auth URL in new browser tab (fallback URL still shown in dialog)
if (data.authUrl) {
+ openedAuthUrlRef.current = true;
window.open(data.authUrl, '_blank');
}
@@ -221,6 +268,7 @@ export function useCliproxyAuthFlow() {
}
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
+ openedAuthUrlRef.current = false;
setState(INITIAL_STATE);
return;
}
@@ -240,6 +288,7 @@ export function useCliproxyAuthFlow() {
const currentProvider = state.provider;
abortControllerRef.current?.abort();
stopPolling();
+ openedAuthUrlRef.current = false;
setState(INITIAL_STATE);
// Also cancel on backend
if (currentProvider) {
diff --git a/ui/src/lib/provider-config.ts b/ui/src/lib/provider-config.ts
index 2da393ba..05e2a958 100644
--- a/ui/src/lib/provider-config.ts
+++ b/ui/src/lib/provider-config.ts
@@ -90,3 +90,67 @@ export const NICKNAME_REQUIRED_PROVIDERS: CLIProxyProvider[] = ['ghcp', 'kiro'];
export function isNicknameRequiredProvider(provider: string): boolean {
return NICKNAME_REQUIRED_PROVIDERS.includes(provider as CLIProxyProvider);
}
+
+/** Kiro auth methods exposed in CCS UI (aligned with CLIProxyAPIPlus support). */
+export const KIRO_AUTH_METHODS = ['aws', 'aws-authcode', 'google', 'github'] as const;
+export type KiroAuthMethod = (typeof KIRO_AUTH_METHODS)[number];
+
+export type KiroFlowType = 'authorization_code' | 'device_code';
+export type KiroStartEndpoint = 'start' | 'start-url';
+
+export interface KiroAuthMethodOption {
+ id: KiroAuthMethod;
+ label: string;
+ description: string;
+ flowType: KiroFlowType;
+ startEndpoint: KiroStartEndpoint;
+}
+
+/** UX-first default for issue #233: AWS Builder ID device flow. */
+export const DEFAULT_KIRO_AUTH_METHOD: KiroAuthMethod = 'aws';
+
+export const KIRO_AUTH_METHOD_OPTIONS: readonly KiroAuthMethodOption[] = [
+ {
+ id: 'aws',
+ label: 'AWS Builder ID (Recommended)',
+ description: 'Device code flow for AWS organizations and Builder ID accounts.',
+ flowType: 'device_code',
+ startEndpoint: 'start',
+ },
+ {
+ id: 'aws-authcode',
+ label: 'AWS Builder ID (Auth Code)',
+ description: 'Authorization code flow via CLI binary.',
+ flowType: 'authorization_code',
+ startEndpoint: 'start',
+ },
+ {
+ id: 'google',
+ label: 'Google OAuth',
+ description: 'Social OAuth flow with callback URL support.',
+ flowType: 'authorization_code',
+ startEndpoint: 'start-url',
+ },
+ {
+ id: 'github',
+ label: 'GitHub OAuth',
+ description: 'Social OAuth flow via management API callback.',
+ flowType: 'authorization_code',
+ startEndpoint: 'start-url',
+ },
+];
+
+export function isKiroAuthMethod(value: string): value is KiroAuthMethod {
+ return KIRO_AUTH_METHODS.includes(value as KiroAuthMethod);
+}
+
+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 getKiroAuthMethodOption(method: KiroAuthMethod): KiroAuthMethodOption {
+ const option = KIRO_AUTH_METHOD_OPTIONS.find((candidate) => candidate.id === method);
+ return option || KIRO_AUTH_METHOD_OPTIONS[0];
+}
From 14dd34187d94fc07419175c17cd893e3f0081537 Mon Sep 17 00:00:00 2001
From: Tam Nhu Tran
Date: Sat, 14 Feb 2026 13:34:08 +0700
Subject: [PATCH 6/7] docs(kiro): document method-aware auth behavior
- add README Kiro auth-method examples and AWS default details
- update architecture docs for method-aware Kiro flow semantics
Refs #552
Refs #233
---
README.md | 17 +++++++++++++++--
docs/system-architecture.md | 24 +++++++++++++++++++-----
2 files changed, 34 insertions(+), 7 deletions(-)
diff --git a/README.md b/README.md
index b4861825..b6a62ffe 100644
--- a/README.md
+++ b/README.md
@@ -60,7 +60,7 @@ Want to run the dashboard in Docker? See `docker/README.md`.
The dashboard provides visual management for all account types:
- **Claude Accounts**: Create isolated instances (work, personal, client)
-- **OAuth Providers**: One-click auth for Gemini, Codex, Antigravity
+- **OAuth Providers**: One-click auth for Gemini, Codex, Antigravity, Kiro, Copilot
- **API Profiles**: Configure GLM, Kimi with your keys
- **Health Monitor**: Real-time status across all profiles
@@ -93,7 +93,7 @@ The dashboard provides visual management for all account types:
| **Codex** | OAuth | `ccs codex` | Code generation |
| **Copilot** | OAuth | `ccs copilot` or `ccs ghcp` | GitHub Copilot models |
| **Cursor IDE** | Local Token | `ccs cursor` | Cursor subscription models via local daemon |
-| **Kiro** | OAuth | `ccs kiro` | AWS CodeWhisperer (Claude-powered) |
+| **Kiro** | OAuth (AWS default) | `ccs kiro` | AWS CodeWhisperer (Claude-powered) |
| **Antigravity** | OAuth | `ccs agy` | Alternative routing |
| **OpenRouter** | API Key | `ccs openrouter` | 300+ models, unified API |
| **Ollama** | Local | `ccs ollama` | Local open-source models, privacy |
@@ -141,6 +141,19 @@ ccs ollama # Local Ollama (no API key needed)
ccs glm # GLM (API key)
```
+### Kiro Auth Methods
+
+`ccs kiro --auth` defaults to AWS Builder ID Device OAuth (best support for AWS org accounts).
+
+```bash
+ccs kiro --auth --kiro-auth-method aws # AWS Builder ID device code (default)
+ccs kiro --auth --kiro-auth-method aws-authcode # AWS Builder ID auth code
+ccs kiro --auth --kiro-auth-method google # Google OAuth
+ccs kiro --auth --kiro-auth-method github # Dashboard management OAuth flow
+```
+
+Dashboard parity: `ccs config` -> Accounts -> Add Kiro account -> choose `Auth Method`.
+
### Cursor IDE Quick Start
```bash
diff --git a/docs/system-architecture.md b/docs/system-architecture.md
index 10b55a2a..96726327 100644
--- a/docs/system-architecture.md
+++ b/docs/system-architecture.md
@@ -86,7 +86,7 @@ CCS v7.34 adds Image Analysis Hook for vision model proxying through CLIProxy wi
v
1. CLIProxy Hardcoded ----+---> gemini, codex, agy, kiro, ghcp
(OAuth-based) | Zero-config OAuth providers
- | (kiro: Auth Code, ghcp: Device Code)
+ | (kiro: method-aware, ghcp: Device Code)
|
2. CLIProxy Variants -----+---> config.cliproxy section
(User-defined) | Custom provider configurations
@@ -570,10 +570,10 @@ CCS v7.34 adds Image Analysis Hook for vision model proxying through CLIProxy wi
| Authentication Flow |
+===========================================================================+
- OAuth Providers - Authorization Code Flow (Gemini, Codex, AGY, Kiro)
- --------------------------------------------------------------------
+ OAuth Providers - Authorization Code Flow (Gemini, Codex, AGY)
+ --------------------------------------------------------------
- 1. User runs: ccs gemini (or ccs kiro)
+ 1. User runs: ccs gemini
|
v
2. Check token cache (~/.ccs/cliproxy/auth/)
@@ -584,7 +584,6 @@ CCS v7.34 adds Image Analysis Hook for vision model proxying through CLIProxy wi
|
v
3. Open browser for OAuth (localhost:PORT callback)
- | - Kiro uses port 9876
v
4. Callback with auth code
|
@@ -595,6 +594,21 @@ CCS v7.34 adds Image Analysis Hook for vision model proxying through CLIProxy wi
6. Cache token locally
+ Kiro OAuth - Method-Aware Flow (CLI + Dashboard parity)
+ -------------------------------------------------------
+
+ Supported methods:
+ - aws: Device Code (default, AWS org friendly)
+ - aws-authcode: Authorization Code via CLI flow
+ - google: Social OAuth via management API
+ - github: Social OAuth via management API (Dashboard flow)
+
+ Key behavior:
+ - Device Code method uses /start route (no callback port)
+ - Callback/social methods use /start-url + status polling
+ - Some management flows return state first, auth_url later
+
+
OAuth Providers - Device Code Flow (GitHub Copilot/ghcp)
--------------------------------------------------------
From e3f0860750e9213572727b8c3f5deb3c57615937 Mon Sep 17 00:00:00 2001
From: Tam Nhu Tran
Date: Sat, 14 Feb 2026 13:36:13 +0700
Subject: [PATCH 7/7] fix(ui): use type-only import for KiroAuthMethod
- satisfy verbatimModuleSyntax during ui build in CI
---
ui/src/components/account/add-account-dialog.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ui/src/components/account/add-account-dialog.tsx b/ui/src/components/account/add-account-dialog.tsx
index eeb6df55..8a250680 100644
--- a/ui/src/components/account/add-account-dialog.tsx
+++ b/ui/src/components/account/add-account-dialog.tsx
@@ -35,8 +35,8 @@ import {
isDeviceCodeProvider,
isNicknameRequiredProvider,
KIRO_AUTH_METHOD_OPTIONS,
- KiroAuthMethod,
} from '@/lib/provider-config';
+import type { KiroAuthMethod } from '@/lib/provider-config';
import { toast } from 'sonner';
interface AddAccountDialogProps {