"use strict";
/**
 * OAuth Handler for CLIProxyAPI
 *
 * Manages OAuth authentication flow for CLIProxy providers (Gemini, Codex, Antigravity, Kiro, Copilot).
 * CLIProxyAPI handles OAuth internally - we just need to:
 * 1. Check if auth exists (token files in CCS auth directory)
 * 2. Trigger OAuth flow by spawning binary with auth flag
 * 3. Auto-detect headless environments (SSH, no DISPLAY)
 * 4. Use --no-browser flag for headless, display OAuth URL for manual auth
 * 5. Handle Device Code flows for Copilot/Qwen (no callback server)
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ensureAuth = exports.triggerOAuth = exports.handlePasteCallbackMode = exports.usesKiroLocalCallbackReplay = exports.resolvePasteCallbackAuthUrl = exports.findNewTokenSnapshotForManualAuth = exports.promptGitLabPersonalAccessToken = exports.normalizeGitLabBaseUrl = exports.getCliAuthNicknameError = exports.requestPasteCallbackStart = exports.getGeminiAuthUrlCredentialError = exports.getGeminiPlusOAuthCredentialError = exports.getPlusAuthUrlCredentialError = exports.getPlusOAuthCredentialError = exports.PLUS_OAUTH_ENV_BY_PROVIDER = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const ui_1 = require("../../utils/ui");
const logging_1 = require("../../services/logging");
const binary_manager_1 = require("../binary-manager");
const config_generator_1 = require("../config/config-generator");
const account_manager_1 = require("../accounts/account-manager");
const oauth_port_diagnostics_1 = require("../../management/oauth-port-diagnostics");
const auth_types_1 = require("./auth-types");
const environment_detector_1 = require("./environment-detector");
const token_manager_1 = require("./token-manager");
const oauth_process_1 = require("./oauth-process");
const kiro_import_1 = require("./kiro-import");
const gitlab_pat_response_1 = require("./gitlab-pat-response");
const oauth_start_failure_guidance_1 = require("./oauth-start-failure-guidance");
const proxy_target_resolver_1 = require("../proxy/proxy-target-resolver");
const account_safety_1 = require("../accounts/account-safety");
const antigravity_responsibility_1 = require("../auth/antigravity-responsibility");
const prompt_1 = require("../../utils/prompt");
const config_manager_1 = require("../../utils/config-manager");
const project_selection_handler_1 = require("./project-selection-handler");
const sink_file_1 = require("./oauth-trace/sink-file");
const oauth_trace_1 = require("./oauth-trace");
const diagnose_failure_1 = require("./oauth-trace/diagnose-failure");
const PASTE_CALLBACK_AUTH_URL_POLL_INTERVAL_MS = 3000;
const POLLED_AUTH_LOCAL_TOKEN_GRACE_MS = 15 * 1000;
const GEMINI_PLUS_CLIENT_ID_ENV = 'CLIPROXY_GEMINI_OAUTH_CLIENT_ID';
const GEMINI_PLUS_CLIENT_SECRET_ENV = 'CLIPROXY_GEMINI_OAUTH_CLIENT_SECRET';
const logger = (0, logging_1.createLogger)('cliproxy:auth:oauth');
/**
 * Table of providers that require Google OAuth client credentials when running
 * against CLIProxy Plus. Keyed by CLIProxyProvider value.
 *
 * Used by the generalized helpers so the dashboard handler can guard any
 * table-listed provider without duplicating env-var names.
 */
exports.PLUS_OAUTH_ENV_BY_PROVIDER = {
    gemini: {
        idEnv: GEMINI_PLUS_CLIENT_ID_ENV,
        secretEnv: GEMINI_PLUS_CLIENT_SECRET_ENV,
        displayName: 'Gemini',
    },
    agy: {
        idEnv: 'CLIPROXY_ANTIGRAVITY_OAUTH_CLIENT_ID',
        secretEnv: 'CLIPROXY_ANTIGRAVITY_OAUTH_CLIENT_SECRET',
        displayName: 'Antigravity',
    },
};
/**
 * Build a human-readable error message for a provider whose Plus OAuth client
 * credentials are missing.
 *
 * @param displayName - Human-readable provider name (e.g. "Gemini", "Antigravity")
 * @param idEnv       - Name of the client-ID env var
 * @param secretEnv   - Name of the client-secret env var
 * @param missing     - Which of the two vars are absent (omit to suppress the "Missing:" prefix)
 */
function buildPlusOAuthCredentialMessage(displayName, idEnv, secretEnv, missing) {
    const missingText = missing?.length ? ` Missing: ${missing.join(', ')}.` : '';
    return (`${displayName} OAuth from CLIProxy Plus is missing Google OAuth client credentials.` +
        missingText +
        ` Set ${idEnv} and ${secretEnv} before starting CLIProxy Plus,` +
        ` or switch \`cliproxy.backend\` to \`original\` for ${displayName}.`);
}
/**
 * Generalized credential-missing guard for any provider in PLUS_OAUTH_ENV_BY_PROVIDER.
 *
 * Returns null when:
 *   - provider is not in the table (not a Plus-credentialed provider)
 *   - backend is not 'plus'
 *   - both credential env vars are set and non-empty
 *
 * Returns an error string when Plus is active and one or both vars are missing.
 */
function getPlusOAuthCredentialError(provider, backend, env = process.env) {
    const entry = exports.PLUS_OAUTH_ENV_BY_PROVIDER[provider];
    if (!entry || backend !== 'plus') {
        return null;
    }
    const missing = [entry.idEnv, entry.secretEnv].filter((name) => !env[name]?.trim());
    return missing.length > 0
        ? buildPlusOAuthCredentialMessage(entry.displayName, entry.idEnv, entry.secretEnv, missing)
        : null;
}
exports.getPlusOAuthCredentialError = getPlusOAuthCredentialError;
/**
 * Generalized auth-URL guard for any provider in PLUS_OAUTH_ENV_BY_PROVIDER.
 *
 * Returns null when:
 *   - provider is not in the table
 *   - authUrl cannot be parsed as a URL (ignore malformed upstream responses)
 *   - client_id query param is present and non-empty
 *
 * Returns an error string when client_id is absent or empty.
 */
function getPlusAuthUrlCredentialError(provider, authUrl) {
    const entry = exports.PLUS_OAUTH_ENV_BY_PROVIDER[provider];
    if (!entry) {
        return null;
    }
    try {
        const parsed = new URL(authUrl);
        const clientId = parsed.searchParams.get('client_id')?.trim();
        return clientId
            ? null
            : buildPlusOAuthCredentialMessage(entry.displayName, entry.idEnv, entry.secretEnv);
    }
    catch {
        return null;
    }
}
exports.getPlusAuthUrlCredentialError = getPlusAuthUrlCredentialError;
// ---------------------------------------------------------------------------
// Gemini-specific aliases — kept for backward-compat with PR #1131's callers.
// These lock the provider to 'gemini' and delegate to the generalized helpers.
// ---------------------------------------------------------------------------
function getGeminiPlusOAuthCredentialError(provider, backend, env = process.env) {
    if (provider !== 'gemini') {
        return null;
    }
    return getPlusOAuthCredentialError(provider, backend, env);
}
exports.getGeminiPlusOAuthCredentialError = getGeminiPlusOAuthCredentialError;
function getGeminiAuthUrlCredentialError(provider, authUrl) {
    if (provider !== 'gemini') {
        return null;
    }
    return getPlusAuthUrlCredentialError(provider, authUrl);
}
exports.getGeminiAuthUrlCredentialError = getGeminiAuthUrlCredentialError;
async function requestPasteCallbackStart(provider, target, options) {
    let startPath = (0, auth_types_1.getPasteCallbackStartPath)(provider, {
        kiroMethod: options?.kiroMethod,
    });
    if (!startPath) {
        throw new Error(`Paste-callback start is not available for ${provider} with the selected method`);
    }
    const normalizedGitLabBaseUrl = provider === 'gitlab' ? normalizeGitLabBaseUrl(options?.gitlabBaseUrl) : undefined;
    if (normalizedGitLabBaseUrl) {
        startPath += `&base_url=${encodeURIComponent(normalizedGitLabBaseUrl)}`;
    }
    const response = await fetch((0, proxy_target_resolver_1.buildProxyUrl)(target, startPath), {
        headers: (0, proxy_target_resolver_1.buildManagementHeaders)(target),
    });
    if (!response.ok) {
        throw new Error(`OAuth start failed with status ${response.status}`);
    }
    return (await response.json());
}
exports.requestPasteCallbackStart = requestPasteCallbackStart;
function getCliAuthNicknameError(provider, nickname, existingAccounts, allowExistingAccountId) {
    if (!nickname || !account_manager_1.PROVIDERS_WITHOUT_EMAIL.includes(provider)) {
        return null;
    }
    const validationError = (0, account_manager_1.validateNickname)(nickname);
    if (validationError) {
        return validationError;
    }
    if ((0, account_manager_1.hasAccountNameConflict)(existingAccounts, nickname, allowExistingAccountId)) {
        return `Nickname "${nickname}" is already in use. Choose a different one.`;
    }
    return null;
}
exports.getCliAuthNicknameError = getCliAuthNicknameError;
function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
function parseAuthUrlState(url) {
    if (!url) {
        return null;
    }
    try {
        return new URL(url).searchParams.get('state');
    }
    catch {
        return null;
    }
}
function normalizeGitLabBaseUrl(baseUrl) {
    const normalized = baseUrl?.trim();
    if (!normalized) {
        return undefined;
    }
    let parsed;
    try {
        parsed = new URL(normalized);
    }
    catch {
        throw new Error('GitLab URL must be a valid http:// or https:// URL');
    }
    if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
        throw new Error('GitLab URL must use http:// or https://');
    }
    parsed.hash = '';
    parsed.search = '';
    parsed.username = '';
    parsed.password = '';
    const normalizedPath = parsed.pathname.replace(/\/+$/, '');
    return normalizedPath ? `${parsed.origin}${normalizedPath}` : parsed.origin;
}
exports.normalizeGitLabBaseUrl = normalizeGitLabBaseUrl;
async function promptGitLabPersonalAccessToken() {
    try {
        const token = (await prompt_1.InteractivePrompt.password('GitLab Personal Access Token')).trim();
        return token.length > 0 ? token : null;
    }
    catch (error) {
        if (error.message.includes('TTY')) {
            console.log((0, ui_1.fail)('GitLab Personal Access Token prompt requires an interactive TTY. Set the token explicitly or use Browser OAuth.'));
            return null;
        }
        throw error;
    }
}
exports.promptGitLabPersonalAccessToken = promptGitLabPersonalAccessToken;
function findNewTokenSnapshotForManualAuth(provider, tokenDir, knownTokenFiles, expectedAccountId) {
    return (0, token_manager_1.findNewTokenSnapshotForAuthAttempt)(provider, tokenDir, knownTokenFiles, expectedAccountId);
}
exports.findNewTokenSnapshotForManualAuth = findNewTokenSnapshotForManualAuth;
async function waitForManualCallbackToken(provider, target, tokenDir, oauthState, knownTokenFiles, expectedAccountId, timeoutMs, pollIntervalMs = PASTE_CALLBACK_AUTH_URL_POLL_INTERVAL_MS) {
    const deadline = Date.now() + timeoutMs;
    let upstreamCompletedAt = null;
    while (Date.now() < deadline) {
        const tokenSnapshot = findNewTokenSnapshotForManualAuth(provider, tokenDir, knownTokenFiles, expectedAccountId);
        if (tokenSnapshot) {
            return { tokenSnapshot };
        }
        if (oauthState) {
            const response = await fetch((0, proxy_target_resolver_1.buildProxyUrl)(target, `/v0/management/get-auth-status?state=${encodeURIComponent(oauthState)}`), { headers: (0, proxy_target_resolver_1.buildManagementHeaders)(target) });
            if (response.ok) {
                const data = (await response.json());
                if (data.status === 'error') {
                    return {
                        tokenSnapshot: null,
                        error: data.error || 'Authentication failed while waiting for local token persistence',
                    };
                }
                if (data.status === 'ok' && upstreamCompletedAt === null) {
                    upstreamCompletedAt = Date.now();
                }
            }
        }
        if (upstreamCompletedAt !== null &&
            Date.now() - upstreamCompletedAt >= POLLED_AUTH_LOCAL_TOKEN_GRACE_MS) {
            break;
        }
        if (Date.now() + pollIntervalMs >= deadline) {
            break;
        }
        await sleep(pollIntervalMs);
    }
    return { tokenSnapshot: null };
}
async function resolvePasteCallbackAuthUrl(target, startData, timeoutMs, pollIntervalMs = PASTE_CALLBACK_AUTH_URL_POLL_INTERVAL_MS) {
    const authUrl = startData.url || startData.auth_url;
    if (authUrl) {
        return authUrl;
    }
    const state = startData.state;
    if (!state) {
        return null;
    }
    const deadline = Date.now() + timeoutMs;
    while (Date.now() < deadline) {
        const response = await fetch((0, proxy_target_resolver_1.buildProxyUrl)(target, `/v0/management/get-auth-status?state=${encodeURIComponent(state)}`), { headers: (0, proxy_target_resolver_1.buildManagementHeaders)(target) });
        if (response.ok) {
            const statusData = (await response.json());
            const polledAuthUrl = statusData.url || statusData.auth_url;
            if (polledAuthUrl) {
                return polledAuthUrl;
            }
            if (statusData.status === 'error' || statusData.status === 'device_code') {
                return null;
            }
        }
        if (Date.now() + pollIntervalMs >= deadline) {
            break;
        }
        await sleep(pollIntervalMs);
    }
    return null;
}
exports.resolvePasteCallbackAuthUrl = resolvePasteCallbackAuthUrl;
/**
 * Prompt user to add another account
 */
async function promptAddAccount() {
    const readline = await Promise.resolve().then(() => __importStar(require('readline')));
    const rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout,
    });
    return new Promise((resolve) => {
        rl.question('[?] Add another account? (y/N): ', (answer) => {
            rl.close();
            resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
        });
    });
}
/**
 * Prompt user to choose OAuth mode for headless environment
 * Returns 'paste' for paste-callback mode or 'forward' for port-forwarding
 */
async function promptOAuthModeChoice(callbackPort) {
    const readline = await Promise.resolve().then(() => __importStar(require('readline')));
    const rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout,
    });
    console.log('');
    console.log((0, ui_1.info)('Headless environment detected (SSH session)'));
    console.log('    OAuth requires choosing a mode:');
    console.log('');
    console.log('    [1] Paste-callback (recommended for VPS)');
    console.log('        Open URL in any browser, paste redirect URL back');
    console.log('');
    console.log('    [2] Port forwarding (advanced)');
    if (callbackPort) {
        console.log(`        Requires: ssh -L ${callbackPort}:localhost:${callbackPort} <USER>@<HOST>`);
    }
    else {
        console.log('        Requires SSH tunnel to callback port');
    }
    console.log('');
    return new Promise((resolve) => {
        let resolved = false;
        // Handle Ctrl+C gracefully
        rl.on('close', () => {
            if (!resolved) {
                resolved = true;
                resolve('paste'); // Safe default on cancel
            }
        });
        rl.question('[?] Which mode? (1/2): ', (answer) => {
            const choice = answer.trim();
            if (choice !== '1' && choice !== '2') {
                console.log((0, ui_1.info)('Invalid choice, using paste-callback mode'));
            }
            resolved = true;
            rl.close();
            resolve(choice === '2' ? 'forward' : 'paste');
        });
    });
}
/**
 * Run pre-flight OAuth checks
 */
async function runPreflightChecks(provider, oauthConfig) {
    console.log('');
    console.log((0, ui_1.info)(`Pre-flight OAuth check for ${oauthConfig.displayName}...`));
    const preflight = await (0, oauth_port_diagnostics_1.enhancedPreflightOAuthCheck)(provider);
    for (const check of preflight.checks) {
        const icon = check.status === 'ok' ? '[OK]' : check.status === 'warn' ? '[!]' : '[X]';
        console.log(`  ${icon} ${check.name}: ${check.message}`);
        if (check.fixCommand && check.status !== 'ok') {
            console.log(`      Fix: ${check.fixCommand}`);
        }
    }
    if (preflight.firewallWarning) {
        console.log('');
        console.log((0, ui_1.warn)('Windows Firewall may block OAuth callback'));
        console.log('    If auth hangs, run as Administrator:');
        console.log(`    ${(0, ui_1.color)(preflight.firewallFixCommand || '', 'command')}`);
    }
    if (!preflight.ready) {
        console.log('');
        console.log((0, ui_1.fail)('Pre-flight check failed. Resolve issues above and retry.'));
        return false;
    }
    return true;
}
/**
 * Prepare OAuth binary and config
 */
async function prepareBinary(provider, verbose) {
    (0, environment_detector_1.showStep)(1, 4, 'progress', 'Preparing CLIProxy binary...');
    try {
        const binaryPath = await (0, binary_manager_1.ensureCLIProxyBinary)(verbose, { skipAutoUpdate: true });
        process.stdout.write('\x1b[1A\x1b[2K');
        (0, environment_detector_1.showStep)(1, 4, 'ok', 'CLIProxy binary ready');
        const tokenDir = (0, token_manager_1.getProviderTokenDir)(provider);
        fs.mkdirSync(tokenDir, { recursive: true, mode: 0o700 });
        const configPath = (0, config_generator_1.generateConfig)(provider);
        if (verbose) {
            console.error(`[auth] Config generated: ${configPath}`);
        }
        return { binaryPath, tokenDir, configPath };
    }
    catch (error) {
        process.stdout.write('\x1b[1A\x1b[2K');
        (0, environment_detector_1.showStep)(1, 4, 'fail', 'Failed to prepare CLIProxy binary');
        console.error((0, ui_1.fail)(error.message));
        throw error;
    }
}
function buildOAuthArgs(provider, configPath, headless, noIncognito, options = {}) {
    const args = ['--config', configPath];
    if (provider === 'kiro') {
        const method = (0, auth_types_1.normalizeKiroAuthMethod)(options.kiroMethod);
        if (!(0, auth_types_1.isKiroCLIAuthMethod)(method)) {
            throw new Error(`Kiro auth method '${method}' is not supported by CLI flow.`);
        }
        args.push(...(0, auth_types_1.getKiroCLIAuthArgs)(method, {
            idcStartUrl: options.kiroIDCStartUrl,
            idcRegion: options.kiroIDCRegion,
            idcFlow: options.kiroIDCFlow,
        }));
    }
    else {
        args.push((0, auth_types_1.getOAuthConfig)(provider).authFlag);
    }
    if (headless) {
        args.push('--no-browser');
    }
    if (provider === 'kiro' && noIncognito) {
        args.push('--no-incognito');
    }
    return args;
}
function usesKiroLocalCallbackReplay(method, idcFlow) {
    const normalizedMethod = (0, auth_types_1.normalizeKiroAuthMethod)(method);
    if (normalizedMethod === 'aws-authcode') {
        return true;
    }
    return normalizedMethod === 'idc' && (0, auth_types_1.normalizeKiroIDCFlow)(idcFlow) === 'authcode';
}
exports.usesKiroLocalCallbackReplay = usesKiroLocalCallbackReplay;
/**
 * Handle paste-callback mode: show auth URL, prompt for callback paste
 * Uses proxy target resolver to connect to correct CLIProxyAPI instance (local or remote)
 */
function createPasteCallbackTraceRecorder(provider, verbose) {
    const fileSink = process.env['CCS_OAUTH_LOG_FILE'] === '1'
        ? (0, sink_file_1.createFileSink)({ dir: path.join((0, config_manager_1.getCcsDir)(), 'logs') })
        : undefined;
    return (0, oauth_trace_1.createOAuthTraceRecorder)({
        sessionId: (0, project_selection_handler_1.generateSessionId)(),
        provider,
        verbose,
        fileSink,
    });
}
async function promptForPasteCallbackUrl(timeoutMs) {
    const readline = await Promise.resolve().then(() => __importStar(require('readline')));
    const rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout,
    });
    return new Promise((resolve) => {
        let resolved = false;
        rl.on('close', () => {
            if (!resolved) {
                resolved = true;
                resolve(null);
            }
        });
        console.log((0, ui_1.info)('After completing authentication, paste the callback URL here:'));
        rl.question('> ', (answer) => {
            resolved = true;
            rl.close();
            resolve(answer.trim() || null);
        });
        setTimeout(() => {
            if (!resolved) {
                resolved = true;
                rl.close();
                console.log('');
                console.log((0, ui_1.fail)(`Timed out waiting for callback URL (${Math.round(timeoutMs / 60000)} minutes)`));
                resolve(null);
            }
        }, timeoutMs);
    });
}
function printPasteCallbackTraceDiagnosis(trace, provider, verbose) {
    const diagnosis = (0, diagnose_failure_1.diagnoseFailure)(trace.snapshot());
    if (diagnosis.branchId === 'UNKNOWN') {
        return;
    }
    const callbackPort = oauth_port_diagnostics_1.OAUTH_CALLBACK_PORTS[provider] ?? null;
    for (const line of (0, diagnose_failure_1.formatErrorMessage)(diagnosis, {
        verbose,
        platform: process.platform,
        callbackPort,
        provider,
    })) {
        console.log(line);
    }
}
async function handlePasteCallbackMode(provider, oauthConfig, verbose, tokenDir, nickname, expectedAccountId, options) {
    // Resolve CLIProxyAPI target (local or remote based on config)
    const target = (0, proxy_target_resolver_1.getProxyTarget)();
    // OAuth state timeout (10 minutes, matches CLIProxyAPI state TTL)
    const OAUTH_STATE_TIMEOUT_MS = options?.timeoutMs ?? 10 * 60 * 1000;
    const pollIntervalMs = options?.pollIntervalMs ?? PASTE_CALLBACK_AUTH_URL_POLL_INTERVAL_MS;
    const trace = options?.trace ?? createPasteCallbackTraceRecorder(provider, verbose);
    console.log('');
    console.log((0, ui_1.info)(`Starting ${oauthConfig.displayName} OAuth (paste-callback mode)...`));
    try {
        // Request auth URL from CLIProxyAPI management endpoints when the selected
        // provider/method supports the manual start-url contract.
        let startData;
        let startPath = (0, auth_types_1.getPasteCallbackStartPath)(provider, {
            kiroMethod: options?.kiroMethod,
        });
        const normalizedGitLabBaseUrl = provider === 'gitlab' ? normalizeGitLabBaseUrl(options?.gitlabBaseUrl) : undefined;
        if (startPath && normalizedGitLabBaseUrl) {
            startPath += `&base_url=${encodeURIComponent(normalizedGitLabBaseUrl)}`;
        }
        try {
            startData = await requestPasteCallbackStart(provider, target, {
                kiroMethod: options?.kiroMethod,
                gitlabBaseUrl: options?.gitlabBaseUrl,
            });
        }
        catch (error) {
            const startError = error.message;
            console.log((0, ui_1.fail)('Failed to start OAuth flow'));
            if (startPath) {
                const guidance = (0, oauth_start_failure_guidance_1.buildOAuthStartFailureGuidance)(provider, {
                    target,
                    startPath,
                    cause: error,
                    addAccount: options?.add,
                });
                for (const line of (0, oauth_start_failure_guidance_1.formatOAuthStartFailureForCli)(guidance)) {
                    console.log(`    ${line}`);
                }
            }
            (0, account_safety_1.warnPossible403Ban)(provider, startError);
            trace.record(oauth_trace_1.OAuthTracePhase.Error, { startPath }, { message: startError });
            await trace.flush();
            printPasteCallbackTraceDiagnosis(trace, provider, verbose);
            return null;
        }
        const authUrl = await resolvePasteCallbackAuthUrl(target, startData, OAUTH_STATE_TIMEOUT_MS, pollIntervalMs);
        if (!authUrl) {
            console.log((0, ui_1.fail)('No authorization URL received'));
            trace.record(oauth_trace_1.OAuthTracePhase.Error, {}, { message: 'No authorization URL received' });
            await trace.flush();
            printPasteCallbackTraceDiagnosis(trace, provider, verbose);
            return null;
        }
        const authUrlCredentialError = getGeminiAuthUrlCredentialError(provider, authUrl);
        if (authUrlCredentialError) {
            console.log((0, ui_1.fail)(authUrlCredentialError));
            trace.record(oauth_trace_1.OAuthTracePhase.Error, {}, { code: 'GEMINI_PLUS_MISSING_CRED', message: authUrlCredentialError });
            await trace.flush();
            printPasteCallbackTraceDiagnosis(trace, provider, verbose);
            return null;
        }
        const oauthState = startData.state || parseAuthUrlState(authUrl);
        const knownTokenFiles = (0, token_manager_1.listProviderTokenSnapshots)(provider, tokenDir);
        // Display auth URL in box
        console.log('');
        console.log('  ╔══════════════════════════════════════════════════════════════╗');
        console.log('  ║  Open this URL in any browser:                               ║');
        console.log('  ╚══════════════════════════════════════════════════════════════╝');
        console.log('');
        console.log(`    ${authUrl}`);
        console.log('');
        trace.record(oauth_trace_1.OAuthTracePhase.AuthUrlDisplayed, { authUrl });
        // Prompt for callback URL
        trace.record(oauth_trace_1.OAuthTracePhase.PasteCallbackPrompted, { timeoutMs: OAUTH_STATE_TIMEOUT_MS });
        const callbackUrl = await (options?.promptForCallbackUrl ?? promptForPasteCallbackUrl)(OAUTH_STATE_TIMEOUT_MS);
        if (!callbackUrl) {
            console.log((0, ui_1.info)('Cancelled'));
            trace.record(oauth_trace_1.OAuthTracePhase.Cancelled, { reason: 'empty_callback' });
            await trace.flush();
            printPasteCallbackTraceDiagnosis(trace, provider, verbose);
            return null;
        }
        trace.record(oauth_trace_1.OAuthTracePhase.PasteCallbackReceived, { callbackUrl });
        // Validate callback URL
        let code;
        try {
            const parsed = new URL(callbackUrl);
            code = parsed.searchParams.get('code') || undefined;
        }
        catch {
            console.log((0, ui_1.fail)('Invalid URL format'));
            trace.record(oauth_trace_1.OAuthTracePhase.PasteCallbackInvalid, { reason: 'invalid_url' });
            await trace.flush();
            printPasteCallbackTraceDiagnosis(trace, provider, verbose);
            return null;
        }
        if (!code) {
            console.log((0, ui_1.fail)('Invalid callback URL: missing code parameter'));
            trace.record(oauth_trace_1.OAuthTracePhase.PasteCallbackInvalid, { reason: 'missing_code' });
            await trace.flush();
            printPasteCallbackTraceDiagnosis(trace, provider, verbose);
            return null;
        }
        // Submit callback to CLIProxyAPI
        console.log((0, ui_1.info)('Submitting callback...'));
        const callbackProvider = auth_types_1.CLIPROXY_CALLBACK_PROVIDER_MAP[provider] || provider;
        trace.record(oauth_trace_1.OAuthTracePhase.PasteCallbackSubmitted, { provider: callbackProvider });
        const callbackResponse = await fetch((0, proxy_target_resolver_1.buildProxyUrl)(target, (0, auth_types_1.getManagementOAuthCallbackPath)()), {
            method: 'POST',
            headers: (0, proxy_target_resolver_1.buildManagementHeaders)(target, { 'Content-Type': 'application/json' }),
            body: JSON.stringify({
                provider: callbackProvider,
                redirect_url: callbackUrl,
            }),
        });
        const callbackData = (await callbackResponse.json());
        if (!callbackResponse.ok || callbackData.status === 'error') {
            const callbackError = callbackData.error || `OAuth callback failed with status ${callbackResponse.status}`;
            console.log((0, ui_1.fail)(callbackError));
            (0, account_safety_1.warnPossible403Ban)(provider, callbackError);
            trace.record(oauth_trace_1.OAuthTracePhase.Error, { status: callbackResponse.status }, { code: 'CALLBACK_REJECTED', message: callbackError });
            await trace.flush();
            printPasteCallbackTraceDiagnosis(trace, provider, verbose);
            return null;
        }
        console.log((0, ui_1.info)('Callback submitted. Waiting for token exchange...'));
        trace.record(oauth_trace_1.OAuthTracePhase.TokenExchangePending, { state: oauthState ?? undefined });
        const { tokenSnapshot, error: tokenWaitError } = await waitForManualCallbackToken(provider, target, tokenDir, oauthState, knownTokenFiles, expectedAccountId, OAUTH_STATE_TIMEOUT_MS, pollIntervalMs);
        if (tokenWaitError) {
            console.log((0, ui_1.fail)(tokenWaitError));
            (0, account_safety_1.warnPossible403Ban)(provider, tokenWaitError);
            trace.record(oauth_trace_1.OAuthTracePhase.Error, {}, { code: 'CALLBACK_REJECTED', message: tokenWaitError });
            await trace.flush();
            printPasteCallbackTraceDiagnosis(trace, provider, verbose);
            return null;
        }
        if (!tokenSnapshot) {
            console.log((0, ui_1.fail)('Authentication completed upstream, but no new local token was saved for this account. Update CCS/CLIProxy and retry.'));
            trace.record(oauth_trace_1.OAuthTracePhase.TokenFileMissing, {});
            await trace.flush();
            printPasteCallbackTraceDiagnosis(trace, provider, verbose);
            return null;
        }
        trace.record(oauth_trace_1.OAuthTracePhase.TokenFileAppeared, {});
        const account = (0, token_manager_1.registerAccountFromToken)(provider, tokenDir, nickname, verbose, tokenSnapshot.file);
        if (!account) {
            console.log((0, ui_1.fail)('Authenticated token could not be matched to the requested account. Retry the flow.'));
            trace.record(oauth_trace_1.OAuthTracePhase.TokenFileMissing, { reason: 'account_match_failed' });
            await trace.flush();
            printPasteCallbackTraceDiagnosis(trace, provider, verbose);
            return null;
        }
        await trace.flush();
        console.log((0, ui_1.ok)('Authentication successful!'));
        // Account safety: check for cross-provider conflicts
        if (account?.email) {
            const conflicts = (0, account_safety_1.checkNewAccountConflict)(provider, account.email);
            if (conflicts) {
                (0, account_safety_1.warnNewAccountConflict)(account.email, conflicts);
            }
        }
        return account;
    }
    catch (error) {
        if (verbose) {
            console.log((0, ui_1.fail)(`Error: ${error.message}`));
        }
        else {
            console.log((0, ui_1.fail)('OAuth failed. Use --verbose for details.'));
        }
        trace.record(oauth_trace_1.OAuthTracePhase.Error, {}, error);
        await trace.flush();
        printPasteCallbackTraceDiagnosis(trace, provider, verbose);
        return null;
    }
}
exports.handlePasteCallbackMode = handlePasteCallbackMode;
async function handleGitLabPatLogin(provider, oauthConfig, verbose, tokenDir, nickname, expectedAccountId, options) {
    const target = (0, proxy_target_resolver_1.getProxyTarget)();
    const baseUrl = normalizeGitLabBaseUrl(options?.gitlabBaseUrl);
    const knownTokenFiles = (0, token_manager_1.listProviderTokenSnapshots)(provider, tokenDir);
    const suppliedToken = options?.gitlabPersonalAccessToken?.trim();
    const personalAccessToken = suppliedToken || process.env['GITLAB_PERSONAL_ACCESS_TOKEN']?.trim() || undefined;
    let token = personalAccessToken;
    if (!token) {
        console.log('');
        console.log((0, ui_1.info)(`Starting ${oauthConfig.displayName} PAT login...`));
        console.log('Paste a Personal Access Token with api and read_user scopes.');
        token = (await promptGitLabPersonalAccessToken()) || undefined;
    }
    if (!token) {
        console.log((0, ui_1.info)('Cancelled'));
        return null;
    }
    const response = await fetch((0, proxy_target_resolver_1.buildProxyUrl)(target, '/v0/management/gitlab-auth-url'), {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            ...(0, proxy_target_resolver_1.buildManagementHeaders)(target),
        },
        body: JSON.stringify({
            ...(baseUrl ? { base_url: baseUrl } : {}),
            personal_access_token: token,
        }),
    });
    const responseBody = await response.text();
    const parsedResponse = (0, gitlab_pat_response_1.parseGitLabPatAuthResponse)(response.ok, response.status, responseBody, token);
    if (!parsedResponse.ok) {
        console.log((0, ui_1.fail)(parsedResponse.errorMessage));
        return null;
    }
    const tokenSnapshot = (0, token_manager_1.findNewTokenSnapshotForAuthAttempt)(provider, tokenDir, knownTokenFiles, expectedAccountId);
    if (!tokenSnapshot) {
        console.log((0, ui_1.fail)('GitLab PAT login completed, but CCS could not find the saved token file.'));
        return null;
    }
    const account = (0, token_manager_1.registerAccountFromToken)(provider, tokenDir, nickname, verbose, expectedAccountId || tokenSnapshot.file);
    if (!account) {
        console.log((0, ui_1.fail)('Authenticated GitLab token could not be registered as a CCS account.'));
        return null;
    }
    console.log((0, ui_1.ok)('Authentication successful!'));
    return account;
}
/**
 * Trigger OAuth flow for provider
 * Auto-detects headless environment and uses --no-browser flag accordingly
 * Shows real-time step-by-step progress for better user feedback
 * Handles both Authorization Code (callback server) and Device Code (polling) flows
 */
async function triggerOAuth(provider, options = {}) {
    const oauthConfig = (0, auth_types_1.getOAuthConfig)(provider);
    (0, account_safety_1.warnOAuthBanRisk)(provider);
    const oauthStartedAt = Date.now();
    logger.stage('auth', 'cliproxy.oauth.start', 'Triggering OAuth flow', {
        provider,
        add: options.add === true,
        fromUI: options.fromUI === true,
    });
    const { verbose = false, add = false, fromUI = false, noIncognito = true } = options;
    const acceptAgyRisk = options.acceptAgyRisk === true;
    const { nickname } = options;
    const resolvedKiroMethod = provider === 'kiro' ? (0, auth_types_1.normalizeKiroAuthMethod)(options.kiroMethod) : auth_types_1.DEFAULT_KIRO_AUTH_METHOD;
    const resolvedKiroIDCFlow = provider === 'kiro' ? (0, auth_types_1.normalizeKiroIDCFlow)(options.kiroIDCFlow) : auth_types_1.DEFAULT_KIRO_IDC_FLOW;
    const resolvedGitLabAuthMode = provider === 'gitlab' && options.gitlabAuthMode === 'pat' ? 'pat' : 'oauth';
    let resolvedGitLabBaseUrl;
    if (provider === 'gitlab') {
        try {
            resolvedGitLabBaseUrl = normalizeGitLabBaseUrl(options.gitlabBaseUrl);
        }
        catch (error) {
            console.log((0, ui_1.fail)(error.message));
            return null;
        }
    }
    if (provider === 'agy') {
        if (fromUI && !acceptAgyRisk) {
            console.log((0, ui_1.fail)('Antigravity OAuth blocked: responsibility acknowledgement is missing.'));
            return null;
        }
        if (!fromUI) {
            const acknowledged = await (0, antigravity_responsibility_1.ensureCliAntigravityResponsibility)({
                context: 'oauth',
                acceptedByFlag: acceptAgyRisk,
            });
            if (!acknowledged) {
                console.log((0, ui_1.info)('Cancelled'));
                return null;
            }
        }
    }
    // Check for existing accounts
    const existingAccounts = (0, account_manager_1.getProviderAccounts)(provider);
    const existingNameMatch = nickname ? (0, account_manager_1.findAccountNameMatch)(existingAccounts, nickname) : null;
    const nicknameError = !fromUI
        ? getCliAuthNicknameError(provider, nickname, existingAccounts, existingNameMatch?.id)
        : null;
    if (nicknameError) {
        console.log((0, ui_1.fail)(nicknameError));
        return null;
    }
    // Handle --import flag: skip OAuth and import from Kiro IDE directly
    if (options.import && provider === 'kiro') {
        const tokenDir = (0, token_manager_1.getProviderTokenDir)(provider);
        const success = await (0, kiro_import_1.importKiroToken)(verbose);
        if (success) {
            return (0, token_manager_1.registerAccountFromToken)(provider, tokenDir, nickname, verbose, existingNameMatch?.id);
        }
        return null;
    }
    if (provider === 'kiro' && resolvedKiroMethod === 'github') {
        console.log((0, ui_1.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'
        ? (0, auth_types_1.getKiroCallbackPort)(resolvedKiroMethod, { idcFlow: resolvedKiroIDCFlow })
        : oauth_port_diagnostics_1.OAUTH_CALLBACK_PORTS[provider];
    const isCLI = !fromUI;
    const headless = options.headless ?? (0, environment_detector_1.isHeadlessEnvironment)();
    const isDeviceCodeFlow = provider === 'kiro'
        ? (0, auth_types_1.isKiroDeviceCodeMethod)(resolvedKiroMethod, { idcFlow: resolvedKiroIDCFlow })
        : callbackPort === null;
    let selectedPasteCallback = options.pasteCallback === true;
    if (provider === 'kiro' && !(0, auth_types_1.isKiroCLIAuthMethod)(resolvedKiroMethod)) {
        console.log((0, ui_1.fail)(`Kiro auth method '${resolvedKiroMethod}' is not supported by CLI flow.`));
        console.log('    Use Dashboard management OAuth for this method.');
        return null;
    }
    // Interactive mode selection for headless environments
    // Skip if explicit mode flag provided or device code flow (no callback needed)
    if (headless && !selectedPasteCallback && !options.portForward && !isDeviceCodeFlow) {
        // Non-interactive environment (piped input) - default to paste mode
        if (!process.stdin.isTTY) {
            selectedPasteCallback = true;
        }
        else {
            const mode = await promptOAuthModeChoice(callbackPort);
            if (mode === 'paste') {
                selectedPasteCallback = true;
            }
        }
    }
    if (provider === 'gitlab' && resolvedGitLabBaseUrl && !selectedPasteCallback) {
        selectedPasteCallback = true;
        console.log('');
        console.log((0, ui_1.info)('GitLab custom base URL selected. Switching to paste-callback mode for OAuth.'));
    }
    const useSelectedKiroLocalPasteCallback = selectedPasteCallback &&
        provider === 'kiro' &&
        usesKiroLocalCallbackReplay(resolvedKiroMethod, resolvedKiroIDCFlow);
    const useSelectedKiroDirectCliFlow = provider === 'kiro' && (isDeviceCodeFlow || useSelectedKiroLocalPasteCallback);
    if (!(selectedPasteCallback && !useSelectedKiroDirectCliFlow)) {
        const credentialError = getGeminiPlusOAuthCredentialError(provider, (0, binary_manager_1.getStoredConfiguredBackend)());
        if (credentialError) {
            console.log((0, ui_1.fail)(credentialError));
            return null;
        }
    }
    if (existingAccounts.length > 0 && !add) {
        console.log('');
        console.log((0, ui_1.info)(`${existingAccounts.length} account(s) already authenticated for ${oauthConfig.displayName}`));
        if (!(await promptAddAccount())) {
            console.log((0, ui_1.info)('Cancelled'));
            return null;
        }
    }
    if (provider === 'gitlab' && resolvedGitLabAuthMode === 'pat') {
        const tokenDir = (0, token_manager_1.getProviderTokenDir)(provider);
        return handleGitLabPatLogin(provider, oauthConfig, verbose, tokenDir, nickname, existingNameMatch?.id, {
            gitlabBaseUrl: resolvedGitLabBaseUrl,
            gitlabPersonalAccessToken: options.gitlabPersonalAccessToken,
        });
    }
    if (selectedPasteCallback && !useSelectedKiroDirectCliFlow) {
        const tokenDir = (0, token_manager_1.getProviderTokenDir)(provider);
        return handlePasteCallbackMode(provider, oauthConfig, verbose, tokenDir, nickname, existingNameMatch?.id, {
            kiroMethod: provider === 'kiro' ? resolvedKiroMethod : undefined,
            gitlabBaseUrl: provider === 'gitlab' ? resolvedGitLabBaseUrl : undefined,
            add,
        });
    }
    // Pre-flight checks (skip for device code flows which don't need callback ports)
    if (!isDeviceCodeFlow && !(await runPreflightChecks(provider, oauthConfig))) {
        return null;
    }
    console.log('');
    // Prepare binary
    const prepared = await prepareBinary(provider, verbose);
    if (!prepared)
        return null;
    const { binaryPath, tokenDir, configPath } = prepared;
    // Free callback port if needed (only for authorization code flows)
    const localCallbackPort = callbackPort;
    if (localCallbackPort) {
        const killed = (0, environment_detector_1.killProcessOnPort)(localCallbackPort, verbose);
        if (killed && verbose) {
            console.error(`[auth] Freed port ${localCallbackPort} for OAuth callback`);
        }
    }
    const processHeadless = selectedPasteCallback && provider === 'kiro' ? true : headless;
    let args;
    try {
        args = buildOAuthArgs(provider, configPath, processHeadless, noIncognito, {
            kiroMethod: provider === 'kiro' ? resolvedKiroMethod : undefined,
            kiroIDCStartUrl: options.kiroIDCStartUrl,
            kiroIDCRegion: options.kiroIDCRegion,
            kiroIDCFlow: provider === 'kiro' ? resolvedKiroIDCFlow : undefined,
        });
    }
    catch (error) {
        console.log((0, ui_1.fail)(error.message));
        return null;
    }
    // Show step based on flow type
    if (isDeviceCodeFlow) {
        (0, environment_detector_1.showStep)(2, 4, 'progress', `Starting ${oauthConfig.displayName} Device Code flow...`);
        console.log('');
        console.log((0, ui_1.info)('Device Code Flow - follow the instructions below'));
    }
    else {
        (0, environment_detector_1.showStep)(2, 4, 'progress', `Starting callback server on port ${callbackPort}...`);
        // Show headless instructions (only for authorization code flows)
        if (useSelectedKiroLocalPasteCallback) {
            console.log('');
            console.log((0, ui_1.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((0, ui_1.warn)('PORT FORWARDING REQUIRED'));
            console.log(`    OAuth callback uses localhost:${callbackPort} which must be reachable.`);
            console.log('    Run this on your LOCAL machine:');
            console.log(`    ${(0, ui_1.color)(`ssh -L ${callbackPort}:localhost:${callbackPort} <USER>@<HOST>`, 'command')}`);
            console.log('');
        }
    }
    // Execute OAuth process
    const account = await (0, oauth_process_1.executeOAuthProcess)({
        provider,
        binaryPath,
        args,
        tokenDir,
        oauthConfig,
        callbackPort,
        headless: processHeadless,
        verbose,
        isCLI,
        nickname,
        expectedAccountId: existingNameMatch?.id,
        authFlowType: isDeviceCodeFlow ? 'device_code' : 'authorization_code',
        kiroMethod: provider === 'kiro' ? resolvedKiroMethod : undefined,
        manualCallback: useSelectedKiroLocalPasteCallback,
    });
    // Show hint for Kiro users about --no-incognito option (first-time auth only)
    if (account && provider === 'kiro' && !noIncognito) {
        console.log('');
        console.log((0, ui_1.info)('Tip: To save your AWS login credentials for future sessions:'));
        console.log('       Use: ccs kiro --no-incognito');
        console.log('       Or enable "Kiro: Use normal browser" in: ccs config');
    }
    // Account safety: check for cross-provider conflicts
    if (account?.email) {
        const conflicts = (0, account_safety_1.checkNewAccountConflict)(provider, account.email);
        if (conflicts) {
            (0, account_safety_1.warnNewAccountConflict)(account.email, conflicts);
        }
    }
    if (account) {
        logger.stage('auth', 'cliproxy.oauth.success', 'OAuth flow completed successfully', { provider, accountId: account.id }, { latencyMs: Date.now() - oauthStartedAt });
    }
    else {
        logger.stage('cleanup', 'cliproxy.oauth.failed', 'OAuth flow failed or was cancelled', { provider }, { level: 'warn', latencyMs: Date.now() - oauthStartedAt });
    }
    return account;
}
exports.triggerOAuth = triggerOAuth;
/**
 * Ensure provider is authenticated
 * Triggers OAuth flow if not authenticated
 */
async function ensureAuth(provider, options = {}) {
    if ((0, token_manager_1.isAuthenticated)(provider)) {
        logger.stage('auth', 'cliproxy.auth.cached', 'Provider already authenticated', {
            provider,
        });
        if (options.verbose) {
            console.error(`[auth] ${provider} already authenticated`);
        }
        const defaultAccount = (0, account_manager_1.getDefaultAccount)(provider);
        if (defaultAccount) {
            (0, account_manager_1.touchAccount)(provider, options.account || defaultAccount.id);
        }
        return true;
    }
    logger.stage('auth', 'cliproxy.auth.required', 'Provider needs authentication', {
        provider,
    });
    const oauthConfig = (0, auth_types_1.getOAuthConfig)(provider);
    console.log((0, ui_1.info)(`${oauthConfig.displayName} authentication required`));
    const account = await triggerOAuth(provider, options);
    return account !== null;
}
exports.ensureAuth = ensureAuth;
//# sourceMappingURL=oauth-handler.js.map