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();