"use strict";
// backend/src/services/providerManager/connectionTester.ts
// SPDX-License-Identifier: AGPL-3.0-or-later
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 () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.testProviderConnection = testProviderConnection;
const runtimeCapabilities_1 = require("./runtimeCapabilities");
const TEST_REQUEST_TIMEOUT_MS = 10000;
const TEST_TOTAL_TIMEOUT_MS = 15000;
const TEST_RESPONSE_BODY_TIMEOUT_MS = 3000;
async function testProviderConnection(provider) {
    const start = Date.now();
    try {
        const totalTimeoutMs = getTimeoutMs('PROVIDER_TEST_TOTAL_TIMEOUT_MS', TEST_TOTAL_TIMEOUT_MS);
        const result = await withTimeout(runTest(provider), totalTimeoutMs, () => new Error(`Provider connection test timed out after ${totalTimeoutMs / 1000}s`));
        return { ...result, latencyMs: Date.now() - start };
    }
    catch (err) {
        return {
            success: false,
            latencyMs: Date.now() - start,
            error: err.message || 'Connection test failed',
        };
    }
}
async function runTest(provider) {
    if (provider.type === 'bedrock')
        return testBedrock(provider);
    if (provider.type === 'vertex')
        return testVertex(provider);
    if (provider.type === 'ollama')
        return testOllama(provider);
    if ((0, runtimeCapabilities_1.resolveProviderAgentRuntime)(provider) === 'openai-agents-sdk') {
        return testOpenAICompatible(provider);
    }
    return testAnthropic(provider);
}
function getClaudeBaseUrl(provider, defaultBaseUrl) {
    return provider.connection.claudeBaseUrl || provider.connection.baseUrl || defaultBaseUrl;
}
function getOpenAIBaseUrl(provider, defaultBaseUrl) {
    return provider.connection.openaiBaseUrl || provider.connection.baseUrl || defaultBaseUrl;
}
function getOpenAIApiKey(provider) {
    return provider.connection.openaiApiKey || provider.connection.apiKey;
}
async function testAnthropic(provider) {
    const defaultBaseUrl = provider.type === 'deepseek'
        ? 'https://api.deepseek.com/anthropic'
        : 'https://api.anthropic.com';
    const baseUrl = getClaudeBaseUrl(provider, defaultBaseUrl);
    const apiKey = provider.connection.claudeApiKey ||
        (!provider.connection.claudeAuthToken && !(0, runtimeCapabilities_1.sharedKeyShouldUseClaudeAuthToken)(provider.type)
            ? provider.connection.apiKey
            : undefined);
    const authToken = provider.connection.claudeAuthToken ||
        (!provider.connection.claudeApiKey && (0, runtimeCapabilities_1.sharedKeyShouldUseClaudeAuthToken)(provider.type)
            ? provider.connection.apiKey
            : undefined);
    if (!apiKey && !authToken)
        return { success: false, error: 'API key or auth token is required' };
    const url = `${baseUrl.replace(/\/+$/, '')}/v1/messages`;
    const headers = {
        'Content-Type': 'application/json',
        'anthropic-version': '2023-06-01',
    };
    if (apiKey)
        headers['x-api-key'] = apiKey;
    if (authToken)
        headers['Authorization'] = `Bearer ${authToken}`;
    const res = await fetchWithTimeout(url, {
        method: 'POST',
        headers,
        body: JSON.stringify({
            model: provider.models.primary || 'claude-sonnet-4-20250514',
            max_tokens: 1,
            messages: [{ role: 'user', content: 'hi' }],
        }),
    });
    if (res.ok)
        return { success: true, modelVerified: true };
    const body = await safeJson(res);
    if (res.status === 401)
        return { success: false, error: 'Invalid API key (401 Unauthorized)' };
    if (res.status === 403)
        return { success: false, error: 'Access denied (403 Forbidden)' };
    if (res.status === 404)
        return { success: false, error: `Model not found: ${provider.models.primary}` };
    return { success: false, error: body?.error?.message || `API error: ${res.status}` };
}
async function testBedrock(provider) {
    const region = provider.connection.awsRegion || 'us-east-1';
    const baseUrl = provider.connection.baseUrl ||
        `https://bedrock-runtime.${region}.amazonaws.com`;
    if (provider.connection.awsBearerToken) {
        return testBedrockWithBearer(provider, baseUrl);
    }
    if (provider.connection.awsAccessKeyId && provider.connection.awsSecretAccessKey) {
        return testBedrockWithSigV4(provider, baseUrl, region);
    }
    if (provider.connection.awsProfile) {
        return { success: false, error: 'AWS Profile auth requires AWS SDK — use Access Key or Bearer Token for connection test' };
    }
    return { success: false, error: 'No AWS credentials configured (need Bearer Token or Access Key)' };
}
async function testBedrockWithBearer(provider, baseUrl) {
    const model = provider.models.primary || 'anthropic.claude-sonnet-4-20250514-v1:0';
    const url = `${baseUrl.replace(/\/+$/, '')}/model/${model}/invoke`;
    const res = await fetchWithTimeout(url, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${provider.connection.awsBearerToken}`,
        },
        body: JSON.stringify({
            anthropic_version: 'bedrock-2023-05-31',
            max_tokens: 1,
            messages: [{ role: 'user', content: 'hi' }],
        }),
    });
    if (res.ok)
        return { success: true, modelVerified: true };
    if (res.status === 401 || res.status === 403) {
        return { success: false, error: `AWS auth failed (${res.status}) — check Bearer Token` };
    }
    const body = await safeJson(res);
    return { success: false, error: body?.message || `Bedrock error: ${res.status}` };
}
async function testBedrockWithSigV4(provider, baseUrl, region) {
    const model = provider.models.primary || 'anthropic.claude-sonnet-4-20250514-v1:0';
    const payload = JSON.stringify({
        anthropic_version: 'bedrock-2023-05-31',
        max_tokens: 1,
        messages: [{ role: 'user', content: 'hi' }],
    });
    const url = `${baseUrl.replace(/\/+$/, '')}/model/${model}/invoke`;
    const headers = await signAwsRequest({
        method: 'POST',
        url,
        region,
        service: 'bedrock',
        accessKeyId: provider.connection.awsAccessKeyId,
        secretAccessKey: provider.connection.awsSecretAccessKey,
        sessionToken: provider.connection.awsSessionToken,
        body: payload,
    });
    const res = await fetchWithTimeout(url, {
        method: 'POST',
        headers: { ...headers, 'Content-Type': 'application/json' },
        body: payload,
    });
    if (res.ok)
        return { success: true, modelVerified: true };
    if (res.status === 401 || res.status === 403) {
        return { success: false, error: `AWS SigV4 auth failed (${res.status}) — check Access Key / Secret` };
    }
    const body = await safeJson(res);
    return { success: false, error: body?.message || `Bedrock error: ${res.status}` };
}
async function testVertex(provider) {
    const projectId = provider.connection.gcpProjectId;
    const region = provider.connection.gcpRegion || 'us-central1';
    if (!projectId)
        return { success: false, error: 'GCP Project ID is required' };
    // Vertex requires OAuth — can only test if env has GOOGLE_APPLICATION_CREDENTIALS
    // For now, validate the endpoint is reachable
    const model = provider.models.primary || 'claude-sonnet-4@20250514';
    const url = `https://${region}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${region}/publishers/anthropic/models/${model}:streamRawPredict`;
    try {
        const res = await fetchWithTimeout(url, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                anthropic_version: 'vertex-2023-10-16',
                max_tokens: 1,
                messages: [{ role: 'user', content: 'hi' }],
            }),
        });
        // 401/403 means endpoint reachable but auth needed — partial success
        if (res.status === 401 || res.status === 403) {
            return { success: true, modelVerified: false, error: 'Endpoint reachable but OAuth required (run gcloud auth)' };
        }
        if (res.ok)
            return { success: true, modelVerified: true };
        const body = await safeJson(res);
        return { success: false, error: body?.error?.message || `Vertex error: ${res.status}` };
    }
    catch (err) {
        if (err.cause?.code === 'ENOTFOUND') {
            return { success: false, error: `DNS resolution failed — check region: ${region}` };
        }
        throw err;
    }
}
async function testOpenAICompatible(provider) {
    const type = provider.type;
    const defaults = {
        deepseek: 'https://api.deepseek.com/v1',
        openai: 'https://api.openai.com/v1',
        ollama: 'http://localhost:11434/v1',
        custom: '',
    };
    const baseUrl = getOpenAIBaseUrl(provider, defaults[type] || '');
    if (!baseUrl)
        return { success: false, error: 'Base URL is required' };
    if (!provider.models.primary) {
        return { success: false, error: 'Primary model is required' };
    }
    const apiKey = getOpenAIApiKey(provider) || '';
    const headers = { 'Content-Type': 'application/json' };
    if (apiKey)
        headers['Authorization'] = `Bearer ${apiKey}`;
    const protocol = resolveOpenAIProtocolForProvider(provider);
    if (protocol === 'responses') {
        return testOpenAIResponsesProtocol(provider, baseUrl, headers);
    }
    return testOpenAIChatCompletionsProtocol(provider, baseUrl, headers);
}
async function testOpenAIResponsesProtocol(provider, baseUrl, headers) {
    const responsesUrl = `${baseUrl.replace(/\/+$/, '')}/responses`;
    const res = await fetchWithTimeout(responsesUrl, {
        method: 'POST',
        headers,
        body: JSON.stringify({
            model: provider.models.primary,
            input: 'hi',
            max_output_tokens: 1,
        }),
    });
    if (res.ok)
        return { success: true, modelVerified: true };
    return providerFailureFromResponse(res, 'Responses API');
}
async function testOpenAIChatCompletionsProtocol(provider, baseUrl, headers) {
    const chatUrl = `${baseUrl.replace(/\/+$/, '')}/chat/completions`;
    const res = await fetchWithTimeout(chatUrl, {
        method: 'POST',
        headers,
        body: JSON.stringify({
            model: provider.models.primary,
            max_tokens: 1,
            messages: [{ role: 'user', content: 'hi' }],
        }),
    });
    if (res.ok)
        return { success: true, modelVerified: true };
    return providerFailureFromResponse(res, 'Chat Completions API');
}
function resolveOpenAIProtocolForProvider(provider) {
    if (provider.connection.openaiProtocol)
        return provider.connection.openaiProtocol;
    return provider.type === 'openai' ? 'responses' : 'chat_completions';
}
async function testOllama(provider) {
    const baseUrl = getOpenAIBaseUrl(provider, 'http://localhost:11434/v1')
        .replace(/\/v1\/?$/, '');
    const tagsUrl = `${baseUrl.replace(/\/+$/, '')}/api/tags`;
    try {
        const res = await fetchWithTimeout(tagsUrl, { method: 'GET', headers: {} });
        if (!res.ok) {
            return { success: false, error: `Ollama returned ${res.status}` };
        }
        const data = await safeJson(res);
        const models = (data?.models || [])
            .map((m) => m.name || m.model)
            .filter((model) => typeof model === 'string' && model.length > 0);
        const target = provider.models.primary;
        if (target && models.length > 0) {
            const modelResolution = resolveOllamaInstalledModel(target, models);
            if (!modelResolution.success) {
                return {
                    success: false,
                    modelVerified: false,
                    error: modelResolution.error,
                };
            }
            return testOpenAICompatible({
                ...provider,
                models: {
                    ...provider.models,
                    primary: modelResolution.model,
                },
            });
        }
        return {
            success: false,
            modelVerified: false,
            error: 'Connected but no Ollama models were returned',
        };
    }
    catch (err) {
        if (err.cause?.code === 'ECONNREFUSED') {
            return { success: false, error: `Cannot connect to Ollama at ${baseUrl} — is it running?` };
        }
        throw err;
    }
}
function resolveOllamaInstalledModel(target, models) {
    const exactMatch = models.find(m => m === target);
    if (exactMatch)
        return { success: true, model: exactMatch };
    const prefixMatches = models.filter(m => m.startsWith(`${target}:`));
    if (prefixMatches.length === 1) {
        return { success: true, model: prefixMatches[0] };
    }
    if (prefixMatches.length > 1) {
        return {
            success: false,
            error: `Connected but model "${target}" matches multiple installed tags: ${prefixMatches.slice(0, 5).join(', ')}`,
        };
    }
    return {
        success: false,
        error: `Connected but model "${target}" not found. Available: ${models.slice(0, 5).join(', ')}`,
    };
}
async function providerFailureFromResponse(res, protocolLabel) {
    if (res.status === 401) {
        return { success: false, error: 'Invalid API key (401 Unauthorized)' };
    }
    if (res.status === 403) {
        return { success: false, error: 'Access denied (403 Forbidden)' };
    }
    if (res.status === 404) {
        return {
            success: false,
            error: `Model or endpoint not found (${protocolLabel}, 404)`,
        };
    }
    const body = await safeJson(res);
    return {
        success: false,
        error: responseErrorMessage(body) ||
            `${protocolLabel} model probe failed: ${res.status}`,
    };
}
function responseErrorMessage(body) {
    if (!body || typeof body !== 'object')
        return undefined;
    if (typeof body.error === 'string')
        return body.error;
    if (typeof body.error?.message === 'string')
        return body.error.message;
    if (typeof body.message === 'string')
        return body.message;
    return undefined;
}
// --- Utilities ---
async function fetchWithTimeout(url, init) {
    const controller = new AbortController();
    const timeoutMs = getTimeoutMs('PROVIDER_TEST_REQUEST_TIMEOUT_MS', TEST_REQUEST_TIMEOUT_MS);
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    try {
        return await fetch(url, { ...init, signal: controller.signal });
    }
    catch (err) {
        if (err.name === 'AbortError') {
            throw new Error(`Connection timed out after ${timeoutMs / 1000}s`);
        }
        if (err.cause?.code === 'ECONNREFUSED') {
            throw new Error(`Connection refused — server not reachable at ${new URL(url).origin}`);
        }
        if (err.cause?.code === 'ENOTFOUND') {
            throw new Error(`DNS resolution failed for ${new URL(url).hostname}`);
        }
        throw new Error(`Network error: ${err.message}`);
    }
    finally {
        clearTimeout(timer);
    }
}
async function safeJson(res) {
    try {
        const body = await readResponseTextWithTimeout(res);
        if (!body)
            return null;
        return JSON.parse(body);
    }
    catch {
        return null;
    }
}
async function readResponseTextWithTimeout(res) {
    const timeoutMs = getTimeoutMs('PROVIDER_TEST_RESPONSE_BODY_TIMEOUT_MS', TEST_RESPONSE_BODY_TIMEOUT_MS);
    return withTimeout(res.text(), timeoutMs, () => {
        cancelResponseBody(res);
        return new Error(`Response body timed out after ${timeoutMs / 1000}s`);
    });
}
function cancelResponseBody(res) {
    try {
        void res.body?.cancel();
    }
    catch {
        // Best-effort cleanup only; the caller will return a normal test failure.
    }
}
async function withTimeout(promise, timeoutMs, createError) {
    let timer;
    const timeout = new Promise((_resolve, reject) => {
        timer = setTimeout(() => reject(createError()), timeoutMs);
    });
    try {
        return await Promise.race([promise, timeout]);
    }
    finally {
        if (timer)
            clearTimeout(timer);
    }
}
function getTimeoutMs(envName, fallbackMs) {
    const raw = process.env[envName];
    if (!raw)
        return fallbackMs;
    const parsed = Number.parseInt(raw, 10);
    return Number.isFinite(parsed) && parsed > 0 ? parsed : fallbackMs;
}
async function signAwsRequest(params) {
    const { method, url, region, service, accessKeyId, secretAccessKey, sessionToken, body } = params;
    const parsedUrl = new URL(url);
    const now = new Date();
    const amzDate = now.toISOString().replace(/[-:]/g, '').replace(/\.\d+Z$/, 'Z');
    const dateStamp = amzDate.slice(0, 8);
    const host = parsedUrl.host;
    const path = parsedUrl.pathname;
    const headers = {
        'host': host,
        'x-amz-date': amzDate,
        'content-type': 'application/json',
    };
    if (sessionToken)
        headers['x-amz-security-token'] = sessionToken;
    const signedHeaderKeys = Object.keys(headers).sort().join(';');
    const canonicalHeaders = Object.keys(headers).sort()
        .map(k => `${k}:${headers[k]}\n`).join('');
    const payloadHash = await sha256Hex(body);
    const canonicalRequest = [method, path, '', canonicalHeaders, signedHeaderKeys, payloadHash].join('\n');
    const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`;
    const stringToSign = ['AWS4-HMAC-SHA256', amzDate, credentialScope, await sha256Hex(canonicalRequest)].join('\n');
    const signingKey = await getSignatureKey(secretAccessKey, dateStamp, region, service);
    const signature = await hmacHex(signingKey, stringToSign);
    const authHeader = `AWS4-HMAC-SHA256 Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaderKeys}, Signature=${signature}`;
    return {
        'Authorization': authHeader,
        'x-amz-date': amzDate,
        ...(sessionToken ? { 'x-amz-security-token': sessionToken } : {}),
    };
}
async function sha256Hex(data) {
    const crypto = await Promise.resolve().then(() => __importStar(require('crypto')));
    return crypto.createHash('sha256').update(data, 'utf8').digest('hex');
}
async function hmacHex(key, data) {
    const crypto = await Promise.resolve().then(() => __importStar(require('crypto')));
    return crypto.createHmac('sha256', key).update(data, 'utf8').digest('hex');
}
async function hmacDigest(key, data) {
    const crypto = await Promise.resolve().then(() => __importStar(require('crypto')));
    return crypto.createHmac('sha256', key).update(data, 'utf8').digest();
}
async function getSignatureKey(key, dateStamp, region, service) {
    const kDate = await hmacDigest(`AWS4${key}`, dateStamp);
    const kRegion = await hmacDigest(kDate, region);
    const kService = await hmacDigest(kRegion, service);
    return hmacDigest(kService, 'aws4_request');
}
//# sourceMappingURL=connectionTester.js.map