// ---------------------------------------------------------------------------
// HTTP client for verified-skill.com API
// ---------------------------------------------------------------------------
import { createRequire } from "node:module";
import { getDefaultKeychain, } from "../lib/keychain.js";
import { getActiveTenant } from "../lib/active-tenant.js";
// Base URL is overridable via `VSKILL_API_BASE` so tests (and hermetic CI runs)
// can point the CLI at a mock server without touching the production host.
// Resolved lazily so test env mutation is always picked up.
const DEFAULT_BASE_URL = "https://verified-skill.com";
function resolveBaseUrl() {
    return process.env.VSKILL_API_BASE || DEFAULT_BASE_URL;
}
const _authCache = {
    token: null,
    tenant: null,
    loaded: false,
};
/**
 * Test-only override for the keychain provider. Production code never
 * reaches this path; tests inject a fake to avoid touching @napi-rs/keyring
 * or the on-disk fallback.
 */
let _keychainOverride = null;
/** @internal — test hook. */
export function _setKeychainForTests(k) {
    _keychainOverride = k;
}
/** @internal — test hook. Forces the next request to re-read keychain + config. */
export function _resetClientAuthCacheForTests() {
    _authCache.token = null;
    _authCache.tenant = null;
    _authCache.loaded = false;
}
/**
 * 0839 F-004 — production-grade cache invalidation.
 *
 * The auth cache (`_authCache`) loads BOTH the bearer token and the active
 * tenant from the keychain + config exactly once per process. After
 * `vskill auth login` mutates the keychain, the same-process cache still
 * holds pre-login state — so a script doing `vskill auth login && vskill
 * orgs list` from a single Node process never sees the new token. The same
 * applies on logout: a stale cache would keep sending the just-revoked
 * token until the process exits.
 *
 * This function is intentionally exported as a non-test primitive so
 * `auth.ts` can call it after `setVskillToken`, `clearVskillToken`, etc.
 * It is internally identical to `_resetClientAuthCacheForTests`; we keep
 * both names so the test hook stays explicit and the production callsite
 * reads correctly.
 */
export function invalidateAuthCache() {
    _authCache.token = null;
    _authCache.tenant = null;
    _authCache.loaded = false;
}
/**
 * Resolve the Authorization token to send. Prefers `vsk_*`, falls back to
 * `gho_*`. Returns null when nothing is stored (anonymous mode).
 *
 * Cached for the lifetime of the process so a single command invocation
 * hits the keychain at most once (AC-US1-06).
 */
function resolveAuthToken() {
    if (_authCache.loaded)
        return _authCache.token;
    let token = null;
    try {
        const kc = _keychainOverride ?? getDefaultKeychain();
        // Prefer vsk_* (ADR-001 / AC-US5-04).
        token = kc.getVskillToken() ?? kc.getGitHubToken();
    }
    catch {
        token = null;
    }
    _authCache.token = token;
    // Tenant resolution: env override beats config file (mirrors ADR-002
    // step #2 — the lib `getActiveTenant` only reads the config file, so we
    // layer the env check here). Per-command --tenant flags are applied by
    // callers via `buildRequestHeaders({ tenantOverride })`.
    let tenant = null;
    if (process.env.VSKILL_TENANT && process.env.VSKILL_TENANT.length > 0) {
        tenant = process.env.VSKILL_TENANT;
    }
    else {
        try {
            tenant = getActiveTenant();
        }
        catch {
            tenant = null;
        }
    }
    _authCache.tenant = tenant;
    _authCache.loaded = true;
    return token;
}
/**
 * Build the standard request headers for a verified-skill API call:
 *   - Content-Type / User-Agent (always)
 *   - Authorization: Bearer <token> (when a token is stored)
 *   - X-Vskill-Tenant: <slug> (when an active tenant is set or overridden)
 *
 * Callers may pass extra headers via `extra`; those win over defaults.
 */
export function buildRequestHeaders(extra, opts = {}) {
    const token = resolveAuthToken();
    const tenant = opts.tenantOverride !== undefined ? opts.tenantOverride : _authCache.tenant;
    const headers = {
        "Content-Type": "application/json",
        "User-Agent": "vskill-cli",
    };
    if (token) {
        headers["Authorization"] = `Bearer ${token}`;
        if (process.env.VSKILL_DEBUG === "1") {
            const kind = token.startsWith("vsk_") ? "vsk_" : "gho_";
            process.stderr.write(`[auth] using ${kind} token (cached)\n`);
        }
    }
    else if (process.env.VSKILL_DEBUG === "1") {
        process.stderr.write(`[auth] no token, anonymous\n`);
    }
    if (tenant) {
        headers["X-Vskill-Tenant"] = tenant;
        if (process.env.VSKILL_DEBUG === "1") {
            process.stderr.write(`[auth] tenant: ${tenant}\n`);
        }
    }
    if (extra) {
        for (const [k, v] of Object.entries(extra))
            headers[k] = v;
    }
    return headers;
}
const VERSION = (() => {
    try {
        const require = createRequire(import.meta.url);
        return require("../../package.json").version;
    }
    catch {
        return "unknown";
    }
})();
export async function apiRequest(path, options) {
    const url = `${resolveBaseUrl()}${path}`;
    const { tenantOverride, ...fetchOpts } = options ?? {};
    const headers = buildRequestHeaders(fetchOpts.headers, { tenantOverride });
    const res = await fetch(url, {
        ...fetchOpts,
        headers,
    });
    if (!res.ok) {
        const body = await res.text().catch(() => "");
        // 0839 AC-US1-04 / AC-US1-05 / AC-US2-05: surface auth + entitlement
        // failures as structured errors so callers can format them without
        // sniffing the message string. We attach `.status` and the parsed body
        // (when JSON) to the thrown error.
        const err = new Error(`API request failed: ${res.status} ${res.statusText}${body ? ` - ${body}` : ""}`);
        err.status = res.status;
        err.body = body;
        if (body) {
            try {
                err.parsedBody = JSON.parse(body);
            }
            catch {
                /* non-JSON body — leave parsedBody undefined */
            }
        }
        throw err;
    }
    return res.json();
}
/**
 * Search for skills in the registry.
 * Uses the edge-first search endpoint which is resilient to DB load.
 */
export async function searchSkills(query, options) {
    const encoded = encodeURIComponent(query);
    const limit = options?.limit ?? 7;
    const data = await apiRequest(`/api/v1/skills/search?q=${encoded}&limit=${limit}`);
    const items = data.results || data.skills || [];
    const results = items.map((s) => ({
        name: String(s.name || ""),
        author: String(s.author || ""),
        repoUrl: s.repoUrl ? String(s.repoUrl) : undefined,
        tier: String(s.certTier || s.tier || "VERIFIED"),
        score: Number(s.trustScore ?? s.certScore ?? s.score ?? 0),
        description: String(s.description || ""),
        installs: Number(s.vskillInstalls ?? 0),
        githubStars: Number(s.githubStars ?? 0),
        command: s.command ? String(s.command) : null,
        pluginName: s.pluginName ? String(s.pluginName) : null,
        isTainted: !!s.isTainted,
        trustTier: s.trustTier ? String(s.trustTier) : undefined,
        certTier: s.certTier ? String(s.certTier) : undefined,
        isBlocked: !!s.isBlocked,
        threatType: s.threatType ? String(s.threatType) : undefined,
        severity: s.severity ? String(s.severity) : undefined,
        vskillInstalls: s.vskillInstalls != null ? Number(s.vskillInstalls) : undefined,
        ownerSlug: s.ownerSlug ? String(s.ownerSlug) : undefined,
        repoSlug: s.repoSlug ? String(s.repoSlug) : undefined,
        skillSlug: s.skillSlug ? String(s.skillSlug) : undefined,
        currentVersion: s.currentVersion ? String(s.currentVersion) : undefined,
        alternateRepos: Array.isArray(s.alternateRepos) ? s.alternateRepos.map((a) => ({
            ownerSlug: String(a.ownerSlug || ""),
            repoSlug: String(a.repoSlug || ""),
            repoUrl: String(a.repoUrl || ""),
        })) : undefined,
    }));
    return { results, hasMore: data.pagination?.hasMore ?? false };
}
/**
 * Build an API path for a skill, supporting hierarchical names (owner/repo/skill).
 */
function skillApiPath(name) {
    const parts = name.split("/");
    return parts.length === 3
        ? `/api/v1/skills/${parts.map(encodeURIComponent).join("/")}`
        : `/api/v1/skills/${encodeURIComponent(name)}`;
}
/**
 * Get a single skill by name.
 * Supports both flat ("architect") and hierarchical ("owner/repo/architect") names.
 */
export async function getSkill(name) {
    const data = await apiRequest(skillApiPath(name));
    // API wraps the skill object under a "skill" key
    const raw = data.skill ?? data;
    return {
        name: String(raw.name || ""),
        author: String(raw.author || ""),
        tier: String(raw.certTier || raw.tier || "VERIFIED"),
        score: Number(raw.certScore ?? raw.score ?? 0),
        version: String(raw.currentVersion || raw.version || "0.0.0"),
        sha: String(raw.sha || ""),
        description: String(raw.description || ""),
        content: raw.content ? String(raw.content) : undefined,
        installs: Number(raw.vskillInstalls ?? raw.installs ?? 0),
        updatedAt: String(raw.updatedAt || ""),
        repoUrl: raw.repoUrl ? String(raw.repoUrl) : undefined,
        trustTier: raw.trustTier ? String(raw.trustTier) : undefined,
        trustScore: raw.trustScore != null ? Number(raw.trustScore) : undefined,
        provenanceVerified: raw.provenanceVerified != null ? Boolean(raw.provenanceVerified) : undefined,
        command: raw.command ? String(raw.command) : null,
        pluginName: raw.pluginName ? String(raw.pluginName) : null,
    };
}
/**
 * Submit a skill for verification.
 */
export async function submitSkill(data) {
    return apiRequest("/api/v1/submissions", {
        method: "POST",
        body: JSON.stringify(data),
    });
}
/**
 * Get the status of a submission.
 */
export async function getSubmission(id) {
    return apiRequest(`/api/v1/submissions/${encodeURIComponent(id)}`);
}
/**
 * List published versions for a skill.
 * Supports hierarchical names (owner/repo/skill).
 */
export async function getVersions(name) {
    const data = await apiRequest(`${skillApiPath(name)}/versions`);
    const items = Array.isArray(data.versions) ? data.versions : [];
    return items.map((v) => v).map((v) => ({
        version: String(v.version || ""),
        certTier: String(v.certTier || ""),
        createdAt: String(v.createdAt || ""),
        contentHash: v.contentHash ? String(v.contentHash) : undefined,
        certScore: v.certScore != null ? Number(v.certScore) : undefined,
        diffSummary: v.diffSummary != null ? String(v.diffSummary) : null,
    }));
}
/**
 * Get a unified diff between two versions of a skill.
 * Uses the platform `?from=X&to=Y` endpoint.
 */
export async function getVersionDiff(name, from, to) {
    const encoded = [encodeURIComponent(from), encodeURIComponent(to)];
    return apiRequest(`${skillApiPath(name)}/versions/diff?from=${encoded[0]}&to=${encoded[1]}`);
}
/**
 * Report a skill install to the platform with retry.
 * Respects VSKILL_NO_TELEMETRY=1 env var for opt-out.
 * Never throws — all errors are silently swallowed.
 *
 * @param skillName - The skill name to report (e.g., "architect")
 * @param repoUrl - Optional repo URL for fallback matching on the server
 */
export async function reportInstall(skillName, repoUrl, version) {
    const verbose = process.env.VSKILL_DEBUG === "1";
    try {
        if (process.env.VSKILL_NO_TELEMETRY === "1")
            return;
        for (let attempt = 0; attempt < 2; attempt++) {
            try {
                const controller = new AbortController();
                const timeout = setTimeout(() => controller.abort(), 5000);
                try {
                    const res = await fetch(`${resolveBaseUrl()}${skillApiPath(skillName)}/installs`, {
                        method: "POST",
                        headers: buildRequestHeaders(),
                        body: JSON.stringify({
                            ...(repoUrl ? { repoUrl } : {}),
                            ...(version ? { version } : {}),
                            source: "cli",
                            platform: process.platform,
                            cliVersion: VERSION,
                        }),
                        signal: controller.signal,
                    });
                    if (res.ok)
                        return; // Success — done
                    if (verbose)
                        process.stderr.write(`[vskill] install tracking failed: HTTP ${res.status} for ${skillName}\n`);
                    // Server error — retry once
                    if (attempt === 0 && res.status >= 500)
                        continue;
                    return; // Client error or second attempt — give up
                }
                finally {
                    clearTimeout(timeout);
                }
            }
            catch (err) {
                if (verbose)
                    process.stderr.write(`[vskill] install tracking error for ${skillName}: ${err}\n`);
                if (attempt === 0) {
                    await new Promise((r) => setTimeout(r, 500));
                    continue; // Retry after 500ms
                }
            }
        }
    }
    catch {
        // Silent — install tracking must never block CLI
    }
}
/**
 * Check multiple installed skills for available updates in a single request.
 * Throws on error — caller is responsible for handling failures.
 */
export async function checkUpdates(skills) {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), 15000);
    try {
        const res = await fetch(`${resolveBaseUrl()}/api/v1/skills/check-updates`, {
            method: "POST",
            headers: buildRequestHeaders(),
            body: JSON.stringify({
                skills,
                platform: process.platform,
                cliVersion: VERSION,
            }),
            signal: controller.signal,
        });
        if (!res.ok) {
            const body = await res.text().catch(() => "");
            const isJson = body.startsWith("{") || body.startsWith("[");
            const truncated = isJson ? body.slice(0, 200) : `(non-JSON response, ${body.length} bytes)`;
            throw new Error(`API request failed: ${res.status} ${res.statusText} - ${truncated}`);
        }
        const data = (await res.json());
        return data.results ?? [];
    }
    finally {
        clearTimeout(timeout);
    }
}
/**
 * Fetch a multi-file diff between two versions of a skill.
 * Hits the platform `/versions/compare?from=X&to=Y` endpoint which returns
 * GitHub-backed compare data when both versions have valid SHAs (source:"github"),
 * or an LCS fallback on SKILL.md alone (source:"local-content").
 */
export async function compareVersions(skill, from, to) {
    // F-CR-006: encode each path segment separately so skill names containing
    // spaces (` `), `#`, or other reserved characters hit the right endpoint.
    // Whole-string encoding would escape the `/` separators — so we split first,
    // encode per segment, then rejoin on an unencoded `/`.
    const encodedSkill = skill
        .split("/")
        .map(encodeURIComponent)
        .join("/");
    const url = `${resolveBaseUrl()}/api/v1/skills/${encodedSkill}/versions/compare?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`;
    const res = await fetch(url, {
        headers: buildRequestHeaders(),
    });
    if (!res.ok) {
        const body = await res.text().catch(() => "");
        throw new Error(`Compare request failed: ${res.status} ${res.statusText}${body ? ` - ${body.slice(0, 200)}` : ""}`);
    }
    return (await res.json());
}
export async function reportInstallBatch(skills) {
    const verbose = process.env.VSKILL_DEBUG === "1";
    try {
        if (process.env.VSKILL_NO_TELEMETRY === "1")
            return;
        if (skills.length === 0)
            return;
        for (let attempt = 0; attempt < 2; attempt++) {
            try {
                const controller = new AbortController();
                const timeout = setTimeout(() => controller.abort(), 10000);
                try {
                    const res = await fetch(`${resolveBaseUrl()}/api/v1/skills/installs`, {
                        method: "POST",
                        headers: buildRequestHeaders(),
                        body: JSON.stringify({
                            skills,
                            source: "cli",
                            platform: process.platform,
                            cliVersion: VERSION,
                        }),
                        signal: controller.signal,
                    });
                    if (res.ok)
                        return;
                    if (verbose)
                        process.stderr.write(`[vskill] batch install tracking failed: HTTP ${res.status} for ${skills.length} skills\n`);
                    if (attempt === 0 && res.status >= 500)
                        continue;
                    return;
                }
                finally {
                    clearTimeout(timeout);
                }
            }
            catch (err) {
                if (verbose)
                    process.stderr.write(`[vskill] batch install tracking error: ${err}\n`);
                if (attempt === 0) {
                    await new Promise((r) => setTimeout(r, 500));
                    continue;
                }
            }
        }
    }
    catch {
        // Silent — install tracking must never block CLI
    }
}
/**
 * 0839 US-003 / US-004 — list tenants the authenticated user is a member of.
 * Backed by `GET /api/v1/account/tenants` (added in T-002, ADR-003).
 *
 * Anonymous calls are NOT allowed: when no token is present, the endpoint
 * returns 401 (the platform's `requireUserOrGithubBearer` rejects). Callers
 * that want anonymous-safe behaviour should check for a token first.
 */
export async function listTenants() {
    const data = await apiRequest(`/api/v1/account/tenants`);
    const items = Array.isArray(data.tenants) ? data.tenants : [];
    return items.map((t) => ({
        tenantId: String(t.tenantId ?? t.id ?? ""),
        slug: String(t.slug ?? ""),
        name: String(t.name ?? t.slug ?? ""),
        role: String(t.role ?? "member"),
        installationId: t.installationId == null
            ? null
            : typeof t.installationId === "number"
                ? t.installationId
                : String(t.installationId),
    }));
}
/**
 * 0839 US-005 — exchange a `gho_*` token for a `vsk_*` API token.
 * Hits `POST /api/v1/auth/github/exchange-for-vsk-token`. The response
 * `token` is the plaintext `vsk_*` (returned ONCE — only the hash is
 * persisted server-side per AC-US5-02).
 *
 * Failures throw — callers (auth.ts) decide whether to fall back to
 * "legacy mode" (gho_-only) or surface the error.
 */
export async function exchangeForVskToken(githubToken) {
    return apiRequest(`/api/v1/auth/github/exchange-for-vsk-token`, {
        method: "POST",
        body: JSON.stringify({ githubToken }),
    });
}
/**
 * 0839 US-005 / AC-US5-06 — best-effort server-side revocation. The CLI
 * `auth logout` calls this AFTER clearing the local keychain; failure is
 * logged but never blocks logout.
 */
export async function signOutAll() {
    await apiRequest(`/api/v1/auth/sign-out-all`, { method: "DELETE" });
}
//# sourceMappingURL=client.js.map