fix(kiro): harden idc and callback auth paths

- keep headless paste routing aligned with the selected Kiro auth method

- validate local callback replay targets and add prompt cancellation safeguards

- wire IDC params through the dashboard start route and support equals-form CLI flags
This commit is contained in:
Tam Nhu Tran
2026-04-05 02:04:11 -04:00
parent bb7dbd108d
commit f40d435a92
8 changed files with 431 additions and 81 deletions
+30 -31
View File
@@ -343,6 +343,17 @@ function buildOAuthArgs(
return args;
}
export function usesKiroLocalCallbackReplay(
method: OAuthOptions['kiroMethod'],
idcFlow: OAuthOptions['kiroIDCFlow']
): boolean {
const normalizedMethod = normalizeKiroAuthMethod(method);
if (normalizedMethod === 'aws-authcode') {
return true;
}
return normalizedMethod === 'idc' && normalizeKiroIDCFlow(idcFlow) === 'authcode';
}
/**
* Handle paste-callback mode: show auth URL, prompt for callback paste
* Uses proxy target resolver to connect to correct CLIProxyAPI instance (local or remote)
@@ -581,10 +592,7 @@ export async function triggerOAuth(
provider === 'kiro'
? isKiroDeviceCodeMethod(resolvedKiroMethod, { idcFlow: resolvedKiroIDCFlow })
: callbackPort === null;
const useKiroLocalPasteCallback =
options.pasteCallback === true && provider === 'kiro' && !isDeviceCodeFlow;
const useKiroDirectCliFlow =
provider === 'kiro' && (isDeviceCodeFlow || useKiroLocalPasteCallback);
let selectedPasteCallback = options.pasteCallback === true;
if (provider === 'kiro' && !isKiroCLIAuthMethod(resolvedKiroMethod)) {
console.log(fail(`Kiro auth method '${resolvedKiroMethod}' is not supported by CLI flow.`));
@@ -594,34 +602,25 @@ export async function triggerOAuth(
// Interactive mode selection for headless environments
// Skip if explicit mode flag provided or device code flow (no callback needed)
if (headless && !options.pasteCallback && !options.portForward && !isDeviceCodeFlow) {
if (headless && !selectedPasteCallback && !options.portForward && !isDeviceCodeFlow) {
// Non-interactive environment (piped input) - default to paste mode
if (!process.stdin.isTTY) {
const tokenDir = getProviderTokenDir(provider);
return handlePasteCallbackMode(
provider,
oauthConfig,
verbose,
tokenDir,
nickname,
existingNameMatch?.id
);
selectedPasteCallback = true;
} else {
const mode = await promptOAuthModeChoice(callbackPort);
if (mode === 'paste') {
selectedPasteCallback = true;
}
}
const mode = await promptOAuthModeChoice(callbackPort);
if (mode === 'paste') {
const tokenDir = getProviderTokenDir(provider);
return handlePasteCallbackMode(
provider,
oauthConfig,
verbose,
tokenDir,
nickname,
existingNameMatch?.id
);
}
// mode === 'forward' continues to existing port-forwarding flow below
}
const useSelectedKiroLocalPasteCallback =
selectedPasteCallback &&
provider === 'kiro' &&
usesKiroLocalCallbackReplay(resolvedKiroMethod, resolvedKiroIDCFlow);
const useSelectedKiroDirectCliFlow =
provider === 'kiro' && (isDeviceCodeFlow || useSelectedKiroLocalPasteCallback);
if (existingAccounts.length > 0 && !add) {
console.log('');
console.log(
@@ -636,7 +635,7 @@ export async function triggerOAuth(
}
}
if (options.pasteCallback && !useKiroDirectCliFlow) {
if (selectedPasteCallback && !useSelectedKiroDirectCliFlow) {
const tokenDir = getProviderTokenDir(provider);
return handlePasteCallbackMode(
provider,
@@ -671,7 +670,7 @@ export async function triggerOAuth(
}
}
const processHeadless = options.pasteCallback && provider === 'kiro' ? true : headless;
const processHeadless = selectedPasteCallback && provider === 'kiro' ? true : headless;
let args: string[];
try {
args = buildOAuthArgs(provider, configPath, processHeadless, noIncognito, {
@@ -694,7 +693,7 @@ export async function triggerOAuth(
showStep(2, 4, 'progress', `Starting callback server on port ${callbackPort}...`);
// Show headless instructions (only for authorization code flows)
if (useKiroLocalPasteCallback) {
if (useSelectedKiroLocalPasteCallback) {
console.log('');
console.log(info('Paste-callback mode enabled for Kiro CLI auth.'));
console.log(
@@ -728,7 +727,7 @@ export async function triggerOAuth(
expectedAccountId: existingNameMatch?.id,
authFlowType: isDeviceCodeFlow ? 'device_code' : 'authorization_code',
kiroMethod: provider === 'kiro' ? resolvedKiroMethod : undefined,
manualCallback: useKiroLocalPasteCallback,
manualCallback: useSelectedKiroLocalPasteCallback,
});
// Show hint for Kiro users about --no-incognito option (first-time auth only)
+152 -25
View File
@@ -71,6 +71,7 @@ interface ProcessState {
userCode: string | null;
kiroMethodSelectionHandled: boolean;
manualCallbackPrompted: boolean;
cancelManualCallbackPrompt: (() => void) | null;
}
/**
@@ -115,7 +116,86 @@ function resolveAuthFlowType(options: OAuthProcessOptions): 'device_code' | 'aut
return options.authFlowType || OAUTH_FLOW_TYPES[options.provider] || 'authorization_code';
}
async function promptManualCallbackUrl(displayName: string): Promise<string | null> {
export function isLoopbackHost(hostname: string): boolean {
const normalized = hostname.replace(/^\[|\]$/g, '').toLowerCase();
return (
normalized === '127.0.0.1' ||
normalized === 'localhost' ||
normalized === '::1' ||
normalized === '0:0:0:0:0:0:0:1'
);
}
export function getExpectedLocalCallback(authUrl: string): {
origin: string;
pathname: string;
state: string | null;
} | null {
try {
const parsedAuthUrl = new URL(authUrl);
const redirectUriRaw = parsedAuthUrl.searchParams.get('redirect_uri');
if (!redirectUriRaw) {
return null;
}
const redirectUri = new URL(redirectUriRaw);
if (!isLoopbackHost(redirectUri.hostname)) {
return null;
}
return {
origin: redirectUri.origin,
pathname: redirectUri.pathname,
state: parsedAuthUrl.searchParams.get('state'),
};
} catch {
return null;
}
}
export function validateManualCallbackUrl(callbackUrl: string, authUrl: string): string | null {
let parsedCallback: URL;
try {
parsedCallback = new URL(callbackUrl);
} catch {
return 'Invalid callback URL format';
}
if (!parsedCallback.searchParams.get('code')) {
return 'Invalid callback URL: missing code parameter';
}
const expectedCallback = getExpectedLocalCallback(authUrl);
if (!expectedCallback) {
return 'Unable to determine the expected local callback target';
}
if (!isLoopbackHost(parsedCallback.hostname)) {
return 'Callback URL must target the local OAuth callback server';
}
if (
parsedCallback.origin !== expectedCallback.origin ||
parsedCallback.pathname !== expectedCallback.pathname
) {
return 'Callback URL does not match the expected local OAuth callback target';
}
if (expectedCallback.state) {
const callbackState = parsedCallback.searchParams.get('state');
if (callbackState !== expectedCallback.state) {
return 'Callback URL state does not match the active OAuth session';
}
}
return null;
}
async function promptManualCallbackUrl(
displayName: string,
state: ProcessState,
timeoutMs: number
): Promise<string | null> {
const readline = await import('readline');
const rl = readline.createInterface({
input: process.stdin,
@@ -124,53 +204,71 @@ async function promptManualCallbackUrl(displayName: string): Promise<string | nu
return new Promise<string | null>((resolve) => {
let settled = false;
let timeout: ReturnType<typeof setTimeout> | null = null;
const finish = (value: string | null) => {
if (settled) {
return;
}
settled = true;
if (timeout) {
clearTimeout(timeout);
}
state.cancelManualCallbackPrompt = null;
resolve(value);
};
state.cancelManualCallbackPrompt = () => {
if (!settled) {
rl.close();
finish(null);
}
};
rl.on('close', () => {
if (!settled) {
settled = true;
resolve(null);
}
finish(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);
finish(answer.trim() || null);
});
timeout = setTimeout(() => {
if (!settled) {
console.log('');
console.log(fail('Timed out waiting for callback URL'));
rl.close();
}
}, timeoutMs);
});
}
async function replayManualCallback(
oauthConfig: ProviderOAuthConfig,
authProcess: ChildProcess,
output: string,
verbose: boolean
authUrl: string,
verbose: boolean,
state: ProcessState,
timeoutMs: number
): Promise<boolean> {
if (!output.includes('http://') && !output.includes('https://')) {
if (!authUrl.includes('http://') && !authUrl.includes('https://')) {
return false;
}
const callbackUrl = await promptManualCallbackUrl(oauthConfig.displayName);
const callbackUrl = await promptManualCallbackUrl(oauthConfig.displayName, state, timeoutMs);
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'));
const validationError = validateManualCallbackUrl(callbackUrl, authUrl);
if (validationError) {
console.log(fail(validationError));
killWithEscalation(authProcess);
return true;
}
@@ -302,7 +400,14 @@ async function handleStdout(
if (options.manualCallback && !state.manualCallbackPrompted) {
state.manualCallbackPrompted = true;
await replayManualCallback(options.oauthConfig, authProcess, urlMatch[0], options.verbose);
await replayManualCallback(
options.oauthConfig,
authProcess,
urlMatch[0],
options.verbose,
state,
10 * 60 * 1000
);
}
}
}
@@ -535,6 +640,7 @@ export function executeOAuthProcess(options: OAuthProcessOptions): Promise<Accou
userCode: null,
kiroMethodSelectionHandled: false,
manualCallbackPrompted: false,
cancelManualCallbackPrompt: null,
};
// Register session for cancellation support
@@ -570,13 +676,27 @@ export function executeOAuthProcess(options: OAuthProcessOptions): Promise<Accou
await handleStdout(data.toString(), state, options, authProcess, log);
});
authProcess.stderr?.on('data', (data: Buffer) => {
authProcess.stderr?.on('data', async (data: Buffer) => {
const output = data.toString();
state.stderrData += output;
log(`stderr: ${output.trim()}`);
if (headless && !state.urlDisplayed) {
displayUrlFromStderr(output, state, oauthConfig);
}
if (options.manualCallback && !state.manualCallbackPrompted) {
const urlMatch = output.match(/https?:\/\/[^\s]+/);
if (urlMatch) {
state.manualCallbackPrompted = true;
await replayManualCallback(
options.oauthConfig,
authProcess,
urlMatch[0],
options.verbose,
state,
10 * 60 * 1000
);
}
}
});
// Show waiting message after delay
@@ -611,10 +731,15 @@ export function executeOAuthProcess(options: OAuthProcessOptions): Promise<Accou
// Timeout handling
// Device code flows need longer timeout to match CLIProxy binary's polling window (60 attempts × 5s = 300s)
const timeoutMs = headless || isDeviceCodeFlow ? 300000 : 120000;
const timeoutMs = options.manualCallback
? 10 * 60 * 1000
: headless || isDeviceCodeFlow
? 300000
: 120000;
const timeout = setTimeout(() => {
// H7: Clear stdin keepalive interval
if (stdinKeepalive) clearInterval(stdinKeepalive);
state.cancelManualCallbackPrompt?.();
// H5: Remove signal handlers before killing process
process.removeListener('SIGINT', cleanup);
process.removeListener('SIGTERM', cleanup);
@@ -634,6 +759,7 @@ export function executeOAuthProcess(options: OAuthProcessOptions): Promise<Accou
clearTimeout(timeout);
// H7: Clear stdin keepalive interval
if (stdinKeepalive) clearInterval(stdinKeepalive);
state.cancelManualCallbackPrompt?.();
// H5: Remove signal handlers to prevent memory leaks
process.removeListener('SIGINT', cleanup);
process.removeListener('SIGTERM', cleanup);
@@ -696,6 +822,7 @@ export function executeOAuthProcess(options: OAuthProcessOptions): Promise<Accou
clearTimeout(timeout);
// H7: Clear stdin keepalive interval
if (stdinKeepalive) clearInterval(stdinKeepalive);
state.cancelManualCallbackPrompt?.();
// H5: Remove signal handlers to prevent memory leaks
process.removeListener('SIGINT', cleanup);
process.removeListener('SIGTERM', cleanup);
+48 -24
View File
@@ -117,6 +117,34 @@ const DEFAULT_CONFIG: ExecutorConfig = {
pollInterval: 100,
};
export function readOptionValue(
args: string[],
flag: string
): { present: boolean; value?: string; missingValue: boolean } {
const inlinePrefix = `${flag}=`;
const inlineArg = args.find((arg) => arg.startsWith(inlinePrefix));
if (inlineArg !== undefined) {
const value = inlineArg.slice(inlinePrefix.length).trim();
return {
present: true,
value: value.length > 0 ? value : undefined,
missingValue: value.length === 0,
};
}
const index = args.indexOf(flag);
if (index === -1) {
return { present: false, missingValue: false };
}
const next = args[index + 1];
if (!next || next.startsWith('-')) {
return { present: true, missingValue: true };
}
return { present: true, value: next.trim(), missingValue: false };
}
/**
* Execute Claude CLI with CLIProxy (main entry point)
*
@@ -346,10 +374,10 @@ export async function execClaudeWithCLIProxy(
// 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('-')) {
const kiroMethodValue = readOptionValue(argsWithoutProxy, '--kiro-auth-method');
if (kiroMethodValue.present) {
const rawMethod = kiroMethodValue.value;
if (kiroMethodValue.missingValue || !rawMethod) {
console.error(fail('--kiro-auth-method requires a value'));
console.error(' Supported values: aws, aws-authcode, google, github, idc');
process.exitCode = 1;
@@ -366,38 +394,30 @@ export async function execClaudeWithCLIProxy(
}
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) {
const kiroIDCStartUrlValue = readOptionValue(argsWithoutProxy, '--kiro-idc-start-url');
if (kiroIDCStartUrlValue.present && kiroIDCStartUrlValue.value) {
kiroIDCStartUrl = kiroIDCStartUrlValue.value;
} else if (kiroIDCStartUrlValue.present) {
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) {
const kiroIDCRegionValue = readOptionValue(argsWithoutProxy, '--kiro-idc-region');
if (kiroIDCRegionValue.present && kiroIDCRegionValue.value) {
kiroIDCRegion = kiroIDCRegionValue.value;
} else if (kiroIDCRegionValue.present) {
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('-')) {
const kiroIDCFlowValue = readOptionValue(argsWithoutProxy, '--kiro-idc-flow');
if (kiroIDCFlowValue.present) {
const rawFlow = kiroIDCFlowValue.value;
if (kiroIDCFlowValue.missingValue || !rawFlow) {
console.error(fail('--kiro-idc-flow requires a value'));
console.error(' Supported values: authcode, device');
process.exitCode = 1;
@@ -1174,6 +1194,10 @@ export async function execClaudeWithCLIProxy(
];
const claudeArgs = argsWithoutProxy.filter((arg, idx) => {
if (ccsFlags.includes(arg)) return false;
if (arg.startsWith('--kiro-auth-method=')) return false;
if (arg.startsWith('--kiro-idc-start-url=')) return false;
if (arg.startsWith('--kiro-idc-region=')) return false;
if (arg.startsWith('--kiro-idc-flow=')) return false;
if (arg.startsWith('--thinking=')) return false;
if (arg.startsWith('--effort=')) return false;
if (arg.startsWith('--1m=') || arg.startsWith('--no-1m=')) return false;
@@ -45,8 +45,11 @@ import {
CLIPROXY_CALLBACK_PROVIDER_MAP,
CLIPROXY_AUTH_URL_PROVIDER_MAP,
isKiroAuthMethod,
isKiroIDCFlow,
isKiroDeviceCodeMethod,
KiroIDCFlow,
KiroAuthMethod,
normalizeKiroIDCFlow,
normalizeKiroAuthMethod,
toKiroManagementMethod,
} from '../../cliproxy/auth/auth-types';
@@ -256,6 +259,43 @@ function parseKiroMethod(raw: unknown): { method: KiroAuthMethod; invalid: boole
return { method: normalizeKiroAuthMethod(normalized), invalid: false };
}
function parseKiroIDCFlow(raw: unknown): { flow: KiroIDCFlow; invalid: boolean } {
if (raw === undefined || raw === null || raw === '') {
return { flow: normalizeKiroIDCFlow(), invalid: false };
}
if (typeof raw !== 'string') {
return { flow: normalizeKiroIDCFlow(), invalid: true };
}
const normalized = raw.trim().toLowerCase();
if (!isKiroIDCFlow(normalized)) {
return { flow: normalizeKiroIDCFlow(), invalid: true };
}
return { flow: normalizeKiroIDCFlow(normalized), invalid: false };
}
export function getKiroStartIDCValidationError(options: {
kiroMethod: KiroAuthMethod;
kiroIDCStartUrl?: string;
invalidKiroIDCFlow?: boolean;
}): { error: string; code: string } | null {
if (options.kiroMethod !== 'idc') {
return null;
}
if (options.invalidKiroIDCFlow) {
return {
error: 'Invalid kiroIDCFlow. Supported: authcode, device',
code: 'INVALID_KIRO_IDC_FLOW',
};
}
if (!options.kiroIDCStartUrl) {
return {
error: 'Kiro IDC login requires kiroIDCStartUrl',
code: 'MISSING_KIRO_IDC_START_URL',
};
}
return null;
}
export function getStartUrlUnsupportedReason(
provider: CLIProxyProvider,
options?: { kiroMethod?: KiroAuthMethod }
@@ -600,6 +640,13 @@ router.post('/:provider/start', async (req: Request, res: Response): Promise<voi
const noIncognitoBody =
typeof requestBody.noIncognito === 'boolean' ? requestBody.noIncognito : undefined;
const kiroMethodRaw = requestBody.kiroMethod;
const kiroIDCStartUrl =
typeof requestBody.kiroIDCStartUrl === 'string'
? requestBody.kiroIDCStartUrl.trim()
: undefined;
const kiroIDCRegion =
typeof requestBody.kiroIDCRegion === 'string' ? requestBody.kiroIDCRegion.trim() : undefined;
const kiroIDCFlowRaw = requestBody.kiroIDCFlow;
const riskAcknowledgement = requestBody.riskAcknowledgement;
const target = getProxyTarget();
if (target.isRemote) {
@@ -609,6 +656,7 @@ router.post('/:provider/start', async (req: Request, res: Response): Promise<voi
// Trim nickname for consistency with CLI (oauth-handler.ts trims input)
const nickname = nicknameRaw?.trim();
const { method: kiroMethod, invalid: invalidKiroMethod } = parseKiroMethod(kiroMethodRaw);
const { flow: kiroIDCFlow, invalid: invalidKiroIDCFlow } = parseKiroIDCFlow(kiroIDCFlowRaw);
// Validate provider
if (!validProviders.includes(provider as CLIProxyProvider)) {
@@ -624,6 +672,18 @@ router.post('/:provider/start', async (req: Request, res: Response): Promise<voi
return;
}
if (provider === 'kiro') {
const kiroIDCValidationError = getKiroStartIDCValidationError({
kiroMethod,
kiroIDCStartUrl,
invalidKiroIDCFlow,
});
if (kiroIDCValidationError) {
res.status(400).json(kiroIDCValidationError);
return;
}
}
if (provider === 'agy' && !isAntigravityResponsibilityBypassEnabled()) {
const validation = validateAntigravityRiskAcknowledgement(riskAcknowledgement);
if (!validation.valid) {
@@ -662,6 +722,9 @@ router.post('/:provider/start', async (req: Request, res: Response): Promise<voi
nickname: nickname || undefined,
acceptAgyRisk: provider === 'agy',
kiroMethod: provider === 'kiro' ? kiroMethod : undefined,
kiroIDCStartUrl: provider === 'kiro' ? kiroIDCStartUrl : undefined,
kiroIDCRegion: provider === 'kiro' ? kiroIDCRegion : undefined,
kiroIDCFlow: provider === 'kiro' && kiroMethod === 'idc' ? kiroIDCFlow : undefined,
fromUI: true, // Enable project selection prompt in UI
noIncognito, // Kiro: use normal browser if enabled
});
@@ -0,0 +1,33 @@
import { describe, expect, it } from 'bun:test';
import { readOptionValue } from '../../../src/cliproxy/executor/index';
describe('readOptionValue', () => {
it('parses split-token option values', () => {
expect(readOptionValue(['--kiro-idc-start-url', 'https://d-123.awsapps.com/start'], '--kiro-idc-start-url')).toEqual({
present: true,
value: 'https://d-123.awsapps.com/start',
missingValue: false,
});
});
it('parses equals-form option values', () => {
expect(readOptionValue(['--kiro-idc-flow=device'], '--kiro-idc-flow')).toEqual({
present: true,
value: 'device',
missingValue: false,
});
});
it('marks empty or missing values as invalid', () => {
expect(readOptionValue(['--kiro-idc-region'], '--kiro-idc-region')).toEqual({
present: true,
value: undefined,
missingValue: true,
});
expect(readOptionValue(['--kiro-idc-flow='], '--kiro-idc-flow')).toEqual({
present: true,
value: undefined,
missingValue: true,
});
});
});
@@ -76,6 +76,20 @@ describe('requestPasteCallbackStart', () => {
});
});
describe('usesKiroLocalCallbackReplay', () => {
it('limits local callback replay to CLI auth-code flows', async () => {
const { usesKiroLocalCallbackReplay } = await import(
`../../../src/cliproxy/auth/oauth-handler?kiro-local-callback-mode=${Date.now()}`
);
expect(usesKiroLocalCallbackReplay('aws-authcode', 'authcode')).toBe(true);
expect(usesKiroLocalCallbackReplay('idc', 'authcode')).toBe(true);
expect(usesKiroLocalCallbackReplay('idc', 'device')).toBe(false);
expect(usesKiroLocalCallbackReplay('google', 'authcode')).toBe(false);
expect(usesKiroLocalCallbackReplay('aws', 'authcode')).toBe(false);
});
});
describe('resolvePasteCallbackAuthUrl', () => {
it('returns the immediate auth URL without polling', async () => {
const { resolvePasteCallbackAuthUrl } = await import(
@@ -1,5 +1,9 @@
import { describe, expect, it } from 'bun:test';
import { extractLikelyAuthFailureFromStderr } from '../../../src/cliproxy/auth/oauth-process';
import {
extractLikelyAuthFailureFromStderr,
getExpectedLocalCallback,
validateManualCallbackUrl,
} from '../../../src/cliproxy/auth/oauth-process';
describe('oauth-process stderr parsing', () => {
it('ignores non-ghcp providers', () => {
@@ -33,3 +37,50 @@ describe('oauth-process stderr parsing', () => {
expect((parsed as string).length).toBe(240);
});
});
describe('oauth-process manual callback validation', () => {
const authUrl =
'https://oidc.example.com/authorize?redirect_uri=http%3A%2F%2F127.0.0.1%3A9876%2Foauth%2Fcallback&state=test-state';
it('extracts the expected local callback target from the auth URL', () => {
expect(getExpectedLocalCallback(authUrl)).toEqual({
origin: 'http://127.0.0.1:9876',
pathname: '/oauth/callback',
state: 'test-state',
});
});
it('accepts matching loopback callback URLs', () => {
expect(
validateManualCallbackUrl(
'http://127.0.0.1:9876/oauth/callback?code=abc123&state=test-state',
authUrl
)
).toBeNull();
});
it('rejects non-loopback callback URLs', () => {
expect(
validateManualCallbackUrl(
'https://evil.example.com/oauth/callback?code=abc123&state=test-state',
authUrl
)
).toContain('local OAuth callback server');
});
it('rejects callback URLs with the wrong path or state', () => {
expect(
validateManualCallbackUrl(
'http://127.0.0.1:9876/not-the-callback?code=abc123&state=test-state',
authUrl
)
).toContain('expected local OAuth callback target');
expect(
validateManualCallbackUrl(
'http://127.0.0.1:9876/oauth/callback?code=abc123&state=wrong-state',
authUrl
)
).toContain('state does not match');
});
});
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'bun:test';
import {
getKiroStartIDCValidationError,
getStartAuthFailureMessage,
getStartAuthNicknameError,
getStartUrlUnsupportedReason,
@@ -38,6 +39,44 @@ describe('cliproxy-auth-routes start-url guard', () => {
});
});
describe('cliproxy-auth-routes Kiro IDC start validation', () => {
it('requires an IDC start URL when idc auth is selected', () => {
expect(
getKiroStartIDCValidationError({
kiroMethod: 'idc',
kiroIDCStartUrl: undefined,
invalidKiroIDCFlow: false,
})
).toEqual({
error: 'Kiro IDC login requires kiroIDCStartUrl',
code: 'MISSING_KIRO_IDC_START_URL',
});
});
it('rejects invalid IDC flow values before triggerOAuth is called', () => {
expect(
getKiroStartIDCValidationError({
kiroMethod: 'idc',
kiroIDCStartUrl: 'https://d-123.awsapps.com/start',
invalidKiroIDCFlow: true,
})
).toEqual({
error: 'Invalid kiroIDCFlow. Supported: authcode, device',
code: 'INVALID_KIRO_IDC_FLOW',
});
});
it('allows valid IDC start payloads through', () => {
expect(
getKiroStartIDCValidationError({
kiroMethod: 'idc',
kiroIDCStartUrl: 'https://d-123.awsapps.com/start',
invalidKiroIDCFlow: false,
})
).toBeNull();
});
});
describe('cliproxy-auth-routes start failure messaging', () => {
it('returns ghcp-specific guidance for Copilot verification failures', () => {
expect(getStartAuthFailureMessage('ghcp')).toContain(