"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getStartAuthNicknameError = exports.getStartAuthFailureMessage = exports.getStartUrlUnsupportedReason = exports.getKiroStartIDCValidationError = void 0;
const express_1 = require("express");
const auth_handler_1 = require("../../cliproxy/auth/auth-handler");
const project_selection_handler_1 = require("../../cliproxy/auth/project-selection-handler");
const auth_session_manager_1 = require("../../cliproxy/auth/auth-session-manager");
const stats_fetcher_1 = require("../../cliproxy/services/stats-fetcher");
const account_manager_1 = require("../../cliproxy/accounts/account-manager");
const proxy_target_resolver_1 = require("../../cliproxy/proxy/proxy-target-resolver");
const remote_auth_fetcher_1 = require("../../cliproxy/services/remote-auth-fetcher");
const managed_model_prefixes_1 = require("../../cliproxy/ai-providers/managed-model-prefixes");
const quota_response_cache_1 = require("../../cliproxy/quota/quota-response-cache");
const kiro_import_1 = require("../../cliproxy/auth/kiro-import");
const token_manager_1 = require("../../cliproxy/auth/token-manager");
const gitlab_pat_response_1 = require("../../cliproxy/auth/gitlab-pat-response");
const auth_types_1 = require("../../cliproxy/auth/auth-types");
const provider_capabilities_1 = require("../../cliproxy/provider-capabilities");
const profile_detector_1 = require("../../auth/profile-detector");
const antigravity_responsibility_1 = require("../../cliproxy/auth/antigravity-responsibility");
const route_helpers_1 = require("./route-helpers");
const auth_middleware_1 = require("../middleware/auth-middleware");
const config_loader_facade_1 = require("../../config/config-loader-facade");
const oauth_handler_1 = require("../../cliproxy/auth/oauth-handler");
const oauth_start_failure_guidance_1 = require("../../cliproxy/auth/oauth-start-failure-guidance");
const binary_manager_1 = require("../../cliproxy/binary-manager");
const router = (0, express_1.Router)();
const MANUAL_AUTH_STATE_TTL_MS = 10 * 60 * 1000;
const POLLED_AUTH_LOCAL_TOKEN_GRACE_MS = 15 * 1000;
const pendingManualAuthState = new Map();
// Valid providers list - derived from canonical CLIPROXY_PROFILES
const validProviders = [...profile_detector_1.CLIPROXY_PROFILES];
const { respondInternalError } = (0, route_helpers_1.createRouteErrorHelpers)('cliproxy-auth-routes');
router.use((req, res, next) => {
    if ((0, auth_middleware_1.requireLocalAccessWhenAuthDisabled)(req, res, 'CLIProxy auth endpoints require localhost access when dashboard auth is disabled.')) {
        next();
    }
});
function pruneExpiredManualAuthState(now = Date.now()) {
    for (const [state, pending] of pendingManualAuthState.entries()) {
        const authExpired = now - pending.createdAt > MANUAL_AUTH_STATE_TTL_MS;
        const withinLocalTokenGrace = pending.upstreamCompletedAt !== undefined &&
            now - pending.upstreamCompletedAt < POLLED_AUTH_LOCAL_TOKEN_GRACE_MS;
        if (authExpired && !withinLocalTokenGrace) {
            pendingManualAuthState.delete(state);
        }
    }
}
function rememberManualAuthState(state, pending) {
    pruneExpiredManualAuthState();
    pendingManualAuthState.set(state, {
        ...pending,
        createdAt: Date.now(),
    });
}
function getManualAuthState(state) {
    if (!state) {
        return null;
    }
    pruneExpiredManualAuthState();
    const pending = pendingManualAuthState.get(state);
    if (!pending) {
        return null;
    }
    return {
        nickname: pending.nickname,
        expectedAccountId: pending.expectedAccountId,
        createdAt: pending.createdAt,
        upstreamCompletedAt: pending.upstreamCompletedAt,
        knownTokenFiles: pending.knownTokenFiles,
    };
}
function markManualAuthUpstreamCompleted(state, now = Date.now()) {
    pruneExpiredManualAuthState(now);
    const pending = pendingManualAuthState.get(state);
    if (!pending) {
        return null;
    }
    if (pending.upstreamCompletedAt !== undefined) {
        return pending.upstreamCompletedAt;
    }
    pending.upstreamCompletedAt = now;
    pendingManualAuthState.set(state, pending);
    return now;
}
function findNewTokenSnapshotForPendingAuth(provider, pending) {
    return (0, token_manager_1.findNewTokenSnapshot)((0, token_manager_1.listProviderTokenSnapshots)(provider), pending.knownTokenFiles, pending.expectedAccountId);
}
function shouldKeepWaitingForLocalToken(state, pending, now = Date.now()) {
    const upstreamCompletedAt = pending.upstreamCompletedAt ?? markManualAuthUpstreamCompleted(state, now);
    return (upstreamCompletedAt !== null && now - upstreamCompletedAt < POLLED_AUTH_LOCAL_TOKEN_GRACE_MS);
}
function invalidateQuotaForRegisteredAccount(account) {
    (0, quota_response_cache_1.invalidateQuotaCache)(account.provider, account.id);
}
function parseKiroMethod(raw) {
    if (raw === undefined || raw === null) {
        return { method: (0, auth_types_1.normalizeKiroAuthMethod)(), invalid: false };
    }
    if (typeof raw !== 'string') {
        return { method: (0, auth_types_1.normalizeKiroAuthMethod)(), invalid: true };
    }
    if (raw.trim() === '') {
        return { method: (0, auth_types_1.normalizeKiroAuthMethod)(), invalid: false };
    }
    const normalized = raw.trim().toLowerCase();
    if (!(0, auth_types_1.isKiroAuthMethod)(normalized)) {
        return { method: (0, auth_types_1.normalizeKiroAuthMethod)(), invalid: true };
    }
    return { method: (0, auth_types_1.normalizeKiroAuthMethod)(normalized), invalid: false };
}
function parseKiroIDCFlow(raw) {
    if (raw === undefined || raw === null || raw === '') {
        return { flow: (0, auth_types_1.normalizeKiroIDCFlow)(), invalid: false };
    }
    if (typeof raw !== 'string') {
        return { flow: (0, auth_types_1.normalizeKiroIDCFlow)(), invalid: true };
    }
    const normalized = raw.trim().toLowerCase();
    if (!(0, auth_types_1.isKiroIDCFlow)(normalized)) {
        return { flow: (0, auth_types_1.normalizeKiroIDCFlow)(), invalid: true };
    }
    return { flow: (0, auth_types_1.normalizeKiroIDCFlow)(normalized), invalid: false };
}
function parseGitLabAuthMode(raw) {
    if (raw === undefined || raw === null || raw === '') {
        return { mode: 'oauth', invalid: false };
    }
    if (typeof raw !== 'string') {
        return { mode: 'oauth', invalid: true };
    }
    const normalized = raw.trim().toLowerCase();
    if (normalized === 'oauth' || normalized === 'pat') {
        return { mode: normalized, invalid: false };
    }
    return { mode: 'oauth', invalid: true };
}
function getKiroStartIDCValidationError(options) {
    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;
}
exports.getKiroStartIDCValidationError = getKiroStartIDCValidationError;
function getStartUrlUnsupportedReason(provider, options) {
    if (provider === 'kiro') {
        const kiroMethod = options?.kiroMethod ?? (0, auth_types_1.normalizeKiroAuthMethod)();
        if (kiroMethod === 'idc') {
            return "Kiro method 'idc' uses CLI auth flow. Use /api/cliproxy/auth/kiro/start instead.";
        }
        if (kiroMethod === 'aws-authcode') {
            return "Kiro method 'aws-authcode' uses CLI auth flow. Use /api/cliproxy/auth/kiro/start instead.";
        }
        if ((0, auth_types_1.isKiroDeviceCodeMethod)(kiroMethod)) {
            return "Kiro method 'aws' uses Device Code flow. Use /api/cliproxy/auth/kiro/start instead.";
        }
        return null;
    }
    if ((0, provider_capabilities_1.isBrowserUrlAuthProvider)(provider)) {
        return null;
    }
    if ((0, provider_capabilities_1.getOAuthFlowType)(provider) === 'device_code') {
        return `Provider '${provider}' uses Device Code flow. Use /api/cliproxy/auth/${provider}/start instead.`;
    }
    return null;
}
exports.getStartUrlUnsupportedReason = getStartUrlUnsupportedReason;
function getStartAuthFailureMessage(provider) {
    if (provider === 'ghcp') {
        return 'Authentication failed, was cancelled, or GitHub Copilot verification did not complete. Ensure the account has an active Copilot subscription and retry.';
    }
    return 'Authentication failed or was cancelled';
}
exports.getStartAuthFailureMessage = getStartAuthFailureMessage;
function getManualCallbackRegistrationError(provider) {
    if (account_manager_1.PROVIDERS_WITHOUT_EMAIL.includes(provider)) {
        return 'Authenticated token could not be matched to a new account. Retry the flow and choose a different nickname if needed.';
    }
    return 'Authenticated token could not be registered. Retry the flow.';
}
function getStartAuthNicknameError(provider, nickname, existingAccounts, allowExistingAccountId) {
    if (!account_manager_1.PROVIDERS_WITHOUT_EMAIL.includes(provider) || !nickname) {
        return null;
    }
    const validationError = (0, account_manager_1.validateNickname)(nickname);
    if (validationError) {
        return {
            error: validationError,
            code: 'INVALID_NICKNAME',
        };
    }
    if ((0, account_manager_1.hasAccountNameConflict)(existingAccounts, nickname, allowExistingAccountId)) {
        return {
            error: `Nickname "${nickname}" is already in use. Choose a different one.`,
            code: 'NICKNAME_EXISTS',
        };
    }
    return null;
}
exports.getStartAuthNicknameError = getStartAuthNicknameError;
/**
 * GET /api/cliproxy/auth - Get auth status for built-in CLIProxy profiles
 * Also fetches CLIProxyAPI stats to update lastUsedAt for active providers
 */
router.get('/', async (_req, res) => {
    try {
        // Check if remote mode is enabled
        const target = (0, proxy_target_resolver_1.getProxyTarget)();
        if (target.isRemote) {
            const authStatus = await (0, remote_auth_fetcher_1.fetchRemoteAuthStatus)(target);
            res.json({ authStatus, source: 'remote' });
            return;
        }
        // Local mode: Initialize accounts from existing tokens on first request
        (0, auth_handler_1.initializeAccounts)();
        // Fetch CLIProxyAPI usage stats to determine active providers
        const stats = await (0, stats_fetcher_1.fetchCliproxyStats)();
        // Update lastUsedAt for providers with recent activity
        if (stats?.requestsByProvider) {
            for (const [statsProvider, requestCount] of Object.entries(stats.requestsByProvider)) {
                if (requestCount > 0) {
                    const provider = (0, provider_capabilities_1.mapExternalProviderName)(statsProvider.toLowerCase());
                    if (provider) {
                        // Touch the default account for this provider (or all accounts)
                        const accounts = (0, account_manager_1.getProviderAccounts)(provider);
                        for (const account of accounts) {
                            // Only touch if this is the default account (most likely being used)
                            if (account.isDefault) {
                                (0, account_manager_1.touchAccount)(provider, account.id);
                            }
                        }
                    }
                }
            }
        }
        const statuses = (0, auth_handler_1.getAllAuthStatus)();
        const authStatus = statuses.map((status) => {
            const oauthConfig = (0, auth_handler_1.getOAuthConfig)(status.provider);
            return {
                provider: status.provider,
                displayName: oauthConfig.displayName,
                authenticated: status.authenticated,
                lastAuth: status.lastAuth?.toISOString() || null,
                tokenFiles: status.tokenFiles.length,
                accounts: status.accounts,
                defaultAccount: status.defaultAccount,
            };
        });
        res.json({ authStatus });
    }
    catch (error) {
        // Return appropriate error for remote vs local mode
        const target = (0, proxy_target_resolver_1.getProxyTarget)();
        if (target.isRemote) {
            res.status(503).json({
                error: 'Failed to fetch remote auth status',
                authStatus: [],
                source: 'remote',
            });
        }
        else {
            respondInternalError(res, error, 'Failed to fetch auth status.');
        }
    }
});
// ==================== Account Management ====================
/**
 * GET /api/cliproxy/accounts - Get all accounts across all providers
 */
router.get('/accounts', async (_req, res) => {
    try {
        // Check if remote mode is enabled
        const target = (0, proxy_target_resolver_1.getProxyTarget)();
        if (target.isRemote) {
            const authStatus = await (0, remote_auth_fetcher_1.fetchRemoteAuthStatus)(target);
            const accounts = authStatus.flatMap((status) => status.accounts);
            res.json({ accounts, source: 'remote' });
            return;
        }
        // Local mode: Initialize accounts from existing tokens
        (0, auth_handler_1.initializeAccounts)();
        const accounts = (0, account_manager_1.getAllAccountsSummary)();
        res.json({ accounts });
    }
    catch (error) {
        const target = (0, proxy_target_resolver_1.getProxyTarget)();
        if (target.isRemote) {
            res.status(503).json({
                error: 'Failed to fetch remote account status',
                accounts: [],
                source: 'remote',
            });
        }
        else {
            respondInternalError(res, error, 'Failed to list accounts.');
        }
    }
});
/**
 * GET /api/cliproxy/accounts/:provider - Get accounts for a specific provider
 */
router.get('/accounts/:provider', (req, res) => {
    const { provider } = req.params;
    // Validate provider
    if (!validProviders.includes(provider)) {
        res.status(400).json({ error: `Invalid provider: ${provider}` });
        return;
    }
    try {
        const accounts = (0, account_manager_1.getProviderAccounts)(provider);
        res.json({ provider, accounts });
    }
    catch (error) {
        respondInternalError(res, error, 'Failed to get provider accounts.');
    }
});
/**
 * POST /api/cliproxy/accounts/:provider/default - Set default account for provider
 */
router.post('/accounts/:provider/default', (req, res) => {
    // Check if remote mode is enabled - account management not available
    const target = (0, proxy_target_resolver_1.getProxyTarget)();
    if (target.isRemote) {
        res.status(501).json({
            error: 'Account management not available in remote mode',
        });
        return;
    }
    const { provider } = req.params;
    const { accountId } = req.body;
    if (!accountId) {
        res.status(400).json({ error: 'Missing required field: accountId' });
        return;
    }
    // Validate provider
    if (!validProviders.includes(provider)) {
        res.status(400).json({ error: `Invalid provider: ${provider}` });
        return;
    }
    try {
        const success = (0, account_manager_1.setDefaultAccount)(provider, accountId);
        if (success) {
            res.json({ provider, defaultAccount: accountId });
        }
        else {
            res
                .status(404)
                .json({ error: `Account '${accountId}' not found for provider '${provider}'` });
        }
    }
    catch (error) {
        respondInternalError(res, error, 'Failed to set default account.');
    }
});
/**
 * DELETE /api/cliproxy/accounts/:provider/:accountId - Remove an account
 */
router.delete('/accounts/:provider/:accountId', (req, res) => {
    // Check if remote mode is enabled - account management not available
    const target = (0, proxy_target_resolver_1.getProxyTarget)();
    if (target.isRemote) {
        res.status(501).json({
            error: 'Account management not available in remote mode',
        });
        return;
    }
    const { provider, accountId } = req.params;
    // Validate provider
    if (!validProviders.includes(provider)) {
        res.status(400).json({ error: `Invalid provider: ${provider}` });
        return;
    }
    try {
        const success = (0, account_manager_1.removeAccount)(provider, accountId);
        if (success) {
            res.json({ provider, accountId, deleted: true });
        }
        else {
            res
                .status(404)
                .json({ error: `Account '${accountId}' not found for provider '${provider}'` });
        }
    }
    catch (error) {
        respondInternalError(res, error, 'Failed to remove account.');
    }
});
/**
 * POST /api/cliproxy/accounts/:provider/:accountId/pause - Pause an account
 * Paused accounts are skipped during quota rotation
 */
router.post('/accounts/:provider/:accountId/pause', (req, res) => {
    const target = (0, proxy_target_resolver_1.getProxyTarget)();
    if (target.isRemote) {
        res.status(501).json({ error: 'Account management not available in remote mode' });
        return;
    }
    const { provider, accountId } = req.params;
    if (!validProviders.includes(provider)) {
        res.status(400).json({ error: `Invalid provider: ${provider}` });
        return;
    }
    try {
        const success = (0, account_manager_1.pauseAccount)(provider, accountId);
        if (success) {
            res.json({ provider, accountId, paused: true });
        }
        else {
            res
                .status(404)
                .json({ error: `Account '${accountId}' not found for provider '${provider}'` });
        }
    }
    catch (error) {
        respondInternalError(res, error, 'Failed to pause account.');
    }
});
/**
 * POST /api/cliproxy/accounts/:provider/:accountId/resume - Resume a paused account
 */
router.post('/accounts/:provider/:accountId/resume', (req, res) => {
    const target = (0, proxy_target_resolver_1.getProxyTarget)();
    if (target.isRemote) {
        res.status(501).json({ error: 'Account management not available in remote mode' });
        return;
    }
    const { provider, accountId } = req.params;
    if (!validProviders.includes(provider)) {
        res.status(400).json({ error: `Invalid provider: ${provider}` });
        return;
    }
    try {
        const success = (0, account_manager_1.resumeAccount)(provider, accountId);
        if (success) {
            res.json({ provider, accountId, paused: false });
        }
        else {
            res
                .status(404)
                .json({ error: `Account '${accountId}' not found for provider '${provider}'` });
        }
    }
    catch (error) {
        respondInternalError(res, error, 'Failed to resume account.');
    }
});
/**
 * POST /api/cliproxy/auth/:provider/start - Start OAuth flow for a provider
 * Opens browser for authentication and returns account info when complete
 */
router.post('/:provider/start', async (req, res) => {
    const { provider } = req.params;
    const requestBody = req.body && typeof req.body === 'object' ? req.body : {};
    const nicknameRaw = typeof requestBody.nickname === 'string' ? requestBody.nickname : undefined;
    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 gitlabAuthModeRaw = requestBody.gitlabAuthMode;
    const gitlabBaseUrl = typeof requestBody.gitlabBaseUrl === 'string' ? requestBody.gitlabBaseUrl.trim() : undefined;
    const gitlabPersonalAccessToken = typeof requestBody.gitlabPersonalAccessToken === 'string'
        ? requestBody.gitlabPersonalAccessToken.trim()
        : undefined;
    const riskAcknowledgement = requestBody.riskAcknowledgement;
    const target = (0, proxy_target_resolver_1.getProxyTarget)();
    if (target.isRemote) {
        res.status(501).json({ error: 'OAuth start flow not available in remote mode' });
        return;
    }
    // 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);
    const { mode: gitlabAuthMode, invalid: invalidGitLabAuthMode } = parseGitLabAuthMode(gitlabAuthModeRaw);
    // Validate provider
    if (!validProviders.includes(provider)) {
        res.status(400).json({ error: `Invalid provider: ${provider}` });
        return;
    }
    if (provider === 'kiro' && invalidKiroMethod) {
        res.status(400).json({
            error: 'Invalid kiroMethod. Supported: aws, aws-authcode, google, github, idc',
            code: 'INVALID_KIRO_METHOD',
        });
        return;
    }
    if (provider === 'gitlab' && invalidGitLabAuthMode) {
        res.status(400).json({
            error: 'Invalid gitlabAuthMode. Supported: oauth, pat',
            code: 'INVALID_GITLAB_AUTH_MODE',
        });
        return;
    }
    if (provider === 'kiro') {
        const kiroIDCValidationError = getKiroStartIDCValidationError({
            kiroMethod,
            kiroIDCStartUrl,
            invalidKiroIDCFlow,
        });
        if (kiroIDCValidationError) {
            res.status(400).json(kiroIDCValidationError);
            return;
        }
    }
    if (provider === 'agy' && !(0, antigravity_responsibility_1.isAntigravityResponsibilityBypassEnabled)()) {
        const validation = (0, antigravity_responsibility_1.validateAntigravityRiskAcknowledgement)(riskAcknowledgement);
        if (!validation.valid) {
            res.status(400).json({
                error: validation.error,
                code: 'AGY_RISK_ACK_REQUIRED',
            });
            return;
        }
    }
    if (provider === 'gitlab' && gitlabAuthMode === 'pat') {
        if (!gitlabPersonalAccessToken) {
            res.status(400).json({
                error: 'gitlabPersonalAccessToken is required when gitlabAuthMode=pat',
                code: 'MISSING_GITLAB_PAT',
            });
            return;
        }
        try {
            const localProvider = provider;
            const knownTokenFiles = (0, token_manager_1.listProviderTokenSnapshots)(localProvider);
            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({
                    ...(gitlabBaseUrl ? { base_url: gitlabBaseUrl } : {}),
                    personal_access_token: gitlabPersonalAccessToken,
                }),
            });
            const responseBody = await response.text();
            const parsedResponse = (0, gitlab_pat_response_1.parseGitLabPatAuthResponse)(response.ok, response.status, responseBody, gitlabPersonalAccessToken);
            if (!parsedResponse.ok) {
                res.status(response.ok ? 400 : response.status).json({
                    error: parsedResponse.errorMessage || 'GitLab PAT authentication failed',
                });
                return;
            }
            const tokenSnapshot = (0, token_manager_1.findNewTokenSnapshot)((0, token_manager_1.listProviderTokenSnapshots)(localProvider), knownTokenFiles);
            if (!tokenSnapshot) {
                res.status(409).json({
                    error: 'GitLab PAT authentication completed, but CCS could not find the saved token.',
                });
                return;
            }
            const account = (0, token_manager_1.registerAccountFromToken)(localProvider, (0, token_manager_1.getProviderTokenDir)(localProvider), nickname, false, tokenSnapshot.file);
            if (!account) {
                res.status(409).json({
                    error: 'GitLab PAT authentication succeeded, but account registration failed.',
                });
                return;
            }
            try {
                await (0, managed_model_prefixes_1.ensureManagedModelPrefixes)([account.provider]);
            }
            catch {
                // Keep auth success path non-fatal when prefix repair cannot run.
            }
            res.json({
                success: true,
                account: {
                    id: account.id,
                    email: account.email,
                    nickname: account.nickname,
                    provider: account.provider,
                    isDefault: account.isDefault,
                },
            });
            return;
        }
        catch (error) {
            respondInternalError(res, error, 'Failed to start GitLab PAT flow.');
            return;
        }
    }
    const existingAccounts = (0, account_manager_1.getProviderAccounts)(provider);
    const nicknameError = getStartAuthNicknameError(provider, nickname, existingAccounts);
    if (nicknameError) {
        res.status(400).json(nicknameError);
        return;
    }
    // Check Kiro no-incognito setting from config (or request body)
    // Default to true (use normal browser) for reliability - incognito often fails
    let noIncognito = true;
    if (provider === 'kiro') {
        const config = (0, config_loader_facade_1.loadOrCreateUnifiedConfig)();
        noIncognito = noIncognitoBody ?? config.cliproxy?.kiro_no_incognito ?? true;
    }
    try {
        // Trigger OAuth flow - this opens browser and waits for completion
        const account = await (0, auth_handler_1.triggerOAuth)(provider, {
            add: true, // Always add mode from UI
            headless: false, // Force interactive mode
            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,
            gitlabAuthMode: provider === 'gitlab' ? gitlabAuthMode : undefined,
            gitlabBaseUrl: provider === 'gitlab' ? gitlabBaseUrl : undefined,
            fromUI: true, // Enable project selection prompt in UI
            noIncognito, // Kiro: use normal browser if enabled
        });
        if (account) {
            try {
                await (0, managed_model_prefixes_1.ensureManagedModelPrefixes)([account.provider]);
            }
            catch {
                // Keep OAuth success path non-fatal when prefix repair cannot run.
            }
            res.json({
                success: true,
                account: {
                    id: account.id,
                    email: account.email,
                    nickname: account.nickname,
                    provider: account.provider,
                    isDefault: account.isDefault,
                },
            });
        }
        else {
            res.status(400).json({
                error: getStartAuthFailureMessage(provider),
            });
        }
    }
    catch (error) {
        respondInternalError(res, error, 'Failed to start OAuth flow.');
    }
});
/**
 * POST /api/cliproxy/auth/:provider/cancel - Cancel in-progress OAuth flow
 * Terminates the OAuth process for the specified provider
 */
router.post('/:provider/cancel', (req, res) => {
    const { provider } = req.params;
    // Validate provider
    if (!validProviders.includes(provider)) {
        res.status(400).json({ error: `Invalid provider: ${provider}` });
        return;
    }
    // Check if there's an active session
    if (!(0, auth_session_manager_1.hasActiveSession)(provider)) {
        res.status(404).json({ error: 'No active authentication session for this provider' });
        return;
    }
    // Cancel all sessions for this provider
    const cancelledCount = (0, auth_session_manager_1.cancelAllSessionsForProvider)(provider);
    res.json({
        success: true,
        cancelled: cancelledCount,
        provider,
    });
});
/**
 * GET /api/cliproxy/auth/project-selection/:sessionId - Get pending project selection prompt
 * Returns project list for user to select from during OAuth flow
 */
router.get('/project-selection/:sessionId', (req, res) => {
    const { sessionId } = req.params;
    const pending = (0, project_selection_handler_1.getPendingSelection)(sessionId);
    if (pending) {
        res.json(pending);
    }
    else {
        res.status(404).json({ error: 'No pending project selection for this session' });
    }
});
/**
 * POST /api/cliproxy/auth/project-selection/:sessionId - Submit project selection
 * Submits user's project choice during OAuth flow
 */
router.post('/project-selection/:sessionId', (req, res) => {
    const { sessionId } = req.params;
    const { selectedId } = req.body;
    if (!selectedId && selectedId !== '') {
        res.status(400).json({ error: 'selectedId is required (use empty string for default)' });
        return;
    }
    const success = (0, project_selection_handler_1.submitProjectSelection)({ sessionId, selectedId });
    if (success) {
        res.json({ success: true });
    }
    else {
        res.status(404).json({ error: 'No pending project selection for this session' });
    }
});
/**
 * POST /api/cliproxy/auth/kiro/import - Import Kiro token from Kiro IDE
 * Alternative auth path when OAuth callback fails to redirect properly
 */
router.post('/kiro/import', async (_req, res) => {
    // Check if remote mode is enabled - import not available remotely
    const target = (0, proxy_target_resolver_1.getProxyTarget)();
    if (target.isRemote) {
        res.status(501).json({
            error: 'Kiro import not available in remote mode',
        });
        return;
    }
    try {
        const tokenDir = (0, token_manager_1.getProviderTokenDir)('kiro');
        const result = await (0, kiro_import_1.tryKiroImport)(tokenDir, false);
        if (result.success) {
            // Re-initialize accounts to pick up new token
            (0, auth_handler_1.initializeAccounts)();
            // Get the newly added account
            const accounts = (0, account_manager_1.getProviderAccounts)('kiro');
            const newAccount = accounts.find((a) => a.isDefault) || accounts[0];
            res.json({
                success: true,
                account: newAccount
                    ? {
                        id: newAccount.id,
                        email: newAccount.email,
                        provider: 'kiro',
                        isDefault: newAccount.isDefault,
                    }
                    : null,
            });
        }
        else {
            res.status(400).json({
                success: false,
                error: result.error || 'Failed to import Kiro token',
            });
        }
    }
    catch (error) {
        respondInternalError(res, error, 'Failed to import Kiro token.');
    }
});
// ==================== Manual Callback Submission ====================
/**
 * POST /api/cliproxy/auth/:provider/start-url - Start OAuth and return auth URL immediately
 * Unlike /start which blocks until completion, this returns the URL for manual callback flow
 */
router.post('/:provider/start-url', async (req, res) => {
    const { provider } = req.params;
    const requestBody = req.body && typeof req.body === 'object' ? req.body : {};
    const nicknameRaw = typeof requestBody.nickname === 'string' ? requestBody.nickname : undefined;
    const kiroMethodRaw = requestBody.kiroMethod;
    const gitlabAuthModeRaw = requestBody.gitlabAuthMode;
    const gitlabBaseUrl = typeof requestBody.gitlabBaseUrl === 'string' ? requestBody.gitlabBaseUrl.trim() : undefined;
    const riskAcknowledgement = requestBody.riskAcknowledgement;
    const nickname = nicknameRaw?.trim();
    const { method: kiroMethod, invalid: invalidKiroMethod } = parseKiroMethod(kiroMethodRaw);
    const { mode: gitlabAuthMode, invalid: invalidGitLabAuthMode } = parseGitLabAuthMode(gitlabAuthModeRaw);
    // Check remote mode
    const target = (0, proxy_target_resolver_1.getProxyTarget)();
    if (target.isRemote) {
        res.status(501).json({ error: 'Manual OAuth flow not available in remote mode' });
        return;
    }
    // Validate provider
    if (!validProviders.includes(provider)) {
        res.status(400).json({ error: `Invalid provider: ${provider}` });
        return;
    }
    if (provider === 'kiro' && invalidKiroMethod) {
        res.status(400).json({
            error: 'Invalid kiroMethod. Supported: aws, aws-authcode, google, github, idc',
            code: 'INVALID_KIRO_METHOD',
        });
        return;
    }
    if (provider === 'gitlab' && invalidGitLabAuthMode) {
        res.status(400).json({
            error: 'Invalid gitlabAuthMode. Supported: oauth, pat',
            code: 'INVALID_GITLAB_AUTH_MODE',
        });
        return;
    }
    if (provider === 'gitlab' && gitlabAuthMode === 'pat') {
        res.status(400).json({
            error: 'GitLab PAT login must use /api/cliproxy/auth/gitlab/start',
            code: 'GITLAB_PAT_REQUIRES_START',
        });
        return;
    }
    if (provider === 'agy' && !(0, antigravity_responsibility_1.isAntigravityResponsibilityBypassEnabled)()) {
        const validation = (0, antigravity_responsibility_1.validateAntigravityRiskAcknowledgement)(riskAcknowledgement);
        if (!validation.valid) {
            res.status(400).json({
                error: validation.error,
                code: 'AGY_RISK_ACK_REQUIRED',
            });
            return;
        }
    }
    const unsupportedReason = getStartUrlUnsupportedReason(provider, {
        kiroMethod: provider === 'kiro' ? kiroMethod : undefined,
    });
    if (unsupportedReason) {
        res.status(400).json({ error: unsupportedReason });
        return;
    }
    const existingAccounts = (0, account_manager_1.getProviderAccounts)(provider);
    const nicknameError = getStartAuthNicknameError(provider, nickname, existingAccounts);
    if (nicknameError) {
        res.status(400).json(nicknameError);
        return;
    }
    // Phase 3: Pre-fetch credential guard for Plus-backend OAuth providers (gemini, agy).
    // Returns null for providers not in the table or when backend is not 'plus'.
    const credentialError = (0, oauth_handler_1.getPlusOAuthCredentialError)(provider, (0, binary_manager_1.getStoredConfiguredBackend)());
    if (credentialError) {
        console.error(`[cliproxy-auth-routes] start-url credential guard fired for provider=${provider}: ${credentialError}`);
        res.status(400).json({
            error: 'plus_oauth_credentials_missing',
            provider,
            message: credentialError,
        });
        return;
    }
    let startPath = null;
    try {
        const authUrlProvider = auth_types_1.CLIPROXY_AUTH_URL_PROVIDER_MAP[provider] || provider;
        const kiroManagementMethod = provider === 'kiro' ? (0, auth_types_1.toKiroManagementMethod)(kiroMethod) : null;
        const kiroQuery = provider === 'kiro' && kiroManagementMethod
            ? `&method=${encodeURIComponent(kiroManagementMethod)}`
            : '';
        const gitlabQuery = provider === 'gitlab' && gitlabBaseUrl
            ? `&base_url=${encodeURIComponent(gitlabBaseUrl)}`
            : '';
        startPath = `/v0/management/${authUrlProvider}-auth-url?is_webui=true${kiroQuery}${gitlabQuery}`;
        // Call CLIProxyAPI to start OAuth and get auth URL
        // CLIProxyAPI management routes are under /v0/management prefix
        const response = await fetch((0, proxy_target_resolver_1.buildProxyUrl)(target, startPath), {
            headers: (0, proxy_target_resolver_1.buildManagementHeaders)(target),
        });
        if (!response.ok) {
            const error = await response.text();
            const guidance = (0, oauth_start_failure_guidance_1.buildOAuthStartFailureGuidance)(provider, {
                target,
                startPath,
                cause: error || `HTTP ${response.status}`,
            });
            res.status(response.status).json(guidance);
            return;
        }
        const data = (await response.json());
        const authUrl = data.url || data.auth_url;
        // Phase 4: Post-fetch auth-URL guard — detect Plus emitting an OAuth URL with empty client_id.
        // Only fires for table-listed providers (gemini, agy); returns null for all others.
        if (authUrl) {
            const authUrlError = (0, oauth_handler_1.getPlusAuthUrlCredentialError)(provider, authUrl);
            if (authUrlError) {
                const redactedUrl = authUrl.split('?')[0];
                console.error(`[cliproxy-auth-routes] Plus emitted OAuth URL without client_id for provider=${provider} url=${redactedUrl}`);
                res.status(502).json({
                    error: 'plus_oauth_url_missing_client_id',
                    provider,
                    message: authUrlError,
                });
                return;
            }
        }
        const oauthState = data.state || parseAuthUrlState(authUrl);
        // Some upstream flows return state first and provide auth_url in subsequent status polling.
        if (!authUrl && !oauthState) {
            res
                .status(500)
                .json({ error: 'No OAuth state or authorization URL received from CLIProxyAPI' });
            return;
        }
        if (oauthState) {
            rememberManualAuthState(oauthState, {
                nickname: nickname || undefined,
                knownTokenFiles: (0, token_manager_1.listProviderTokenSnapshots)(provider),
            });
        }
        res.json({
            success: true,
            authUrl: authUrl || null,
            state: oauthState,
            method: data.method || null,
        });
    }
    catch (error) {
        if (error instanceof SyntaxError) {
            console.error(`[cliproxy-auth-routes] Invalid OAuth start response for provider=${provider}: ${error.message}`);
            res.status(502).json({
                error: 'cliproxy_oauth_start_invalid_response',
                provider,
                message: 'CLIProxyAPI returned an invalid OAuth start response.',
                details: error.message,
            });
            return;
        }
        const authUrlProvider = auth_types_1.CLIPROXY_AUTH_URL_PROVIDER_MAP[provider] || provider;
        const guidance = (0, oauth_start_failure_guidance_1.buildOAuthStartFailureGuidance)(provider, {
            target,
            startPath: startPath ?? `/v0/management/${authUrlProvider}-auth-url?is_webui=true`,
            cause: error,
        });
        console.error(`[cliproxy-auth-routes] ${guidance.message} ${guidance.details}`);
        res.status(503).json(guidance);
    }
});
/**
 * GET /api/cliproxy/auth/:provider/status - Poll OAuth status
 * Checks if OAuth has completed for the given state
 */
router.get('/:provider/status', async (req, res) => {
    const { provider } = req.params;
    const { state } = req.query;
    if (!state || typeof state !== 'string') {
        res.status(400).json({ error: 'state query parameter required' });
        return;
    }
    // Validate provider
    if (!validProviders.includes(provider)) {
        res.status(400).json({ error: `Invalid provider: ${provider}` });
        return;
    }
    try {
        const target = (0, proxy_target_resolver_1.getProxyTarget)();
        // CLIProxyAPI management routes are under /v0/management prefix
        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) });
        const data = (await response.json());
        if (data.status === 'ok') {
            const localProvider = provider;
            const pendingAuth = getManualAuthState(state);
            if (!pendingAuth) {
                res.status(409).json({
                    status: 'error',
                    error: 'Authentication completed upstream, but CCS could not match it to the active add-account session. Retry the flow from the dashboard.',
                });
                return;
            }
            const now = Date.now();
            const tokenSnapshot = findNewTokenSnapshotForPendingAuth(localProvider, pendingAuth);
            if (!tokenSnapshot) {
                if (shouldKeepWaitingForLocalToken(state, pendingAuth, now)) {
                    res.json({ status: 'wait' });
                    return;
                }
                pendingManualAuthState.delete(state);
                res.status(409).json({
                    status: 'error',
                    error: 'Authentication completed upstream, but no new local token was saved for this account. Update CCS/CLIProxy and retry.',
                });
                return;
            }
            const account = (0, token_manager_1.registerAccountFromToken)(localProvider, (0, token_manager_1.getProviderTokenDir)(localProvider), pendingAuth.nickname, false, tokenSnapshot.file);
            if (!account) {
                pendingManualAuthState.delete(state);
                res.status(409).json({
                    status: 'error',
                    error: getManualCallbackRegistrationError(localProvider),
                });
                return;
            }
            try {
                await (0, managed_model_prefixes_1.ensureManagedModelPrefixes)([account.provider]);
            }
            catch {
                // Keep manual callback success path non-fatal when prefix repair cannot run.
            }
            invalidateQuotaForRegisteredAccount(account);
            res.json({
                status: 'ok',
                account: {
                    id: account.id,
                    email: account.email,
                    nickname: account.nickname,
                    provider: account.provider,
                    isDefault: account.isDefault,
                },
            });
            pendingManualAuthState.delete(state);
            return;
        }
        res.json(data);
    }
    catch {
        res.status(503).json({ status: 'error', error: 'CLIProxyAPI not reachable' });
    }
});
/**
 * Parse OAuth callback URL to extract code and state parameters.
 * @param url - The callback URL to parse
 * @returns Parsed components (code, state) or empty object on failure
 */
function parseCallbackUrl(url) {
    try {
        const parsed = new URL(url);
        return {
            code: parsed.searchParams.get('code') || undefined,
            state: parsed.searchParams.get('state') || undefined,
        };
    }
    catch {
        return {};
    }
}
function parseAuthUrlState(url) {
    if (!url) {
        return null;
    }
    try {
        return new URL(url).searchParams.get('state');
    }
    catch {
        return null;
    }
}
/**
 * POST /api/cliproxy/auth/:provider/submit-callback - Submit OAuth callback URL manually
 * For cross-browser OAuth flows where callback cannot redirect directly
 */
router.post('/:provider/submit-callback', async (req, res) => {
    const { provider } = req.params;
    const { redirectUrl } = req.body;
    // Check remote mode
    const target = (0, proxy_target_resolver_1.getProxyTarget)();
    if (target.isRemote) {
        res.status(501).json({ error: 'Manual callback not available in remote mode' });
        return;
    }
    // Validate provider
    if (!validProviders.includes(provider)) {
        res.status(400).json({ error: `Invalid provider: ${provider}` });
        return;
    }
    // Validate redirectUrl
    if (!redirectUrl || typeof redirectUrl !== 'string') {
        res.status(400).json({ error: 'redirectUrl is required' });
        return;
    }
    const parsed = parseCallbackUrl(redirectUrl);
    if (!parsed.code) {
        res.status(400).json({ error: 'Invalid callback URL: missing code parameter' });
        return;
    }
    const pendingAuth = getManualAuthState(parsed.state);
    try {
        const callbackProvider = auth_types_1.CLIPROXY_CALLBACK_PROVIDER_MAP[provider] || provider;
        // Forward to CLIProxyAPI /oauth-callback endpoint (under /v0/management prefix)
        const response = await fetch((0, proxy_target_resolver_1.buildProxyUrl)(target, '/v0/management/oauth-callback'), {
            method: 'POST',
            headers: (0, proxy_target_resolver_1.buildManagementHeaders)(target, { 'Content-Type': 'application/json' }),
            body: JSON.stringify({
                provider: callbackProvider,
                redirect_url: redirectUrl,
            }),
        });
        const data = (await response.json());
        if (!response.ok || data.status === 'error') {
            res.status(response.status >= 400 ? response.status : 400).json({
                error: data.error || 'OAuth callback failed',
            });
            return;
        }
        const localProvider = provider;
        const now = Date.now();
        if (pendingAuth) {
            const tokenSnapshot = findNewTokenSnapshotForPendingAuth(localProvider, pendingAuth);
            if (!tokenSnapshot) {
                if (parsed.state && shouldKeepWaitingForLocalToken(parsed.state, pendingAuth, now)) {
                    res.json({ status: 'wait' });
                    return;
                }
                if (parsed.state) {
                    pendingManualAuthState.delete(parsed.state);
                }
                res.status(409).json({
                    error: getManualCallbackRegistrationError(localProvider),
                });
                return;
            }
            const account = (0, token_manager_1.registerAccountFromToken)(localProvider, (0, token_manager_1.getProviderTokenDir)(localProvider), pendingAuth.nickname, false, tokenSnapshot.file);
            if (!account) {
                if (parsed.state) {
                    pendingManualAuthState.delete(parsed.state);
                }
                res.status(409).json({
                    error: getManualCallbackRegistrationError(localProvider),
                });
                return;
            }
            if (parsed.state) {
                try {
                    await (0, managed_model_prefixes_1.ensureManagedModelPrefixes)([account.provider]);
                }
                catch {
                    // Keep manual callback success path non-fatal when prefix repair cannot run.
                }
            }
            invalidateQuotaForRegisteredAccount(account);
            res.json({
                success: true,
                account: {
                    id: account.id,
                    email: account.email,
                    nickname: account.nickname,
                    provider: account.provider,
                    isDefault: account.isDefault,
                },
            });
            if (parsed.state) {
                pendingManualAuthState.delete(parsed.state);
            }
            return;
        }
        const account = (0, token_manager_1.registerAccountFromToken)(localProvider, (0, token_manager_1.getProviderTokenDir)(localProvider), undefined, false, undefined);
        if (!account) {
            res.status(409).json({
                error: getManualCallbackRegistrationError(localProvider),
            });
            return;
        }
        invalidateQuotaForRegisteredAccount(account);
        res.json({
            success: true,
            account: {
                id: account.id,
                email: account.email,
                nickname: account.nickname,
                provider: account.provider,
                isDefault: account.isDefault,
            },
        });
    }
    catch (error) {
        respondInternalError(res, error, 'CLIProxyAPI not reachable.', 503);
    }
});
exports.default = router;
//# sourceMappingURL=cliproxy-auth-routes.js.map