/**
 * Kimi Code (subscription) OAuth flow
 *
 * RFC 8628 device authorization grant against https://auth.kimi.com with JSON
 * responses. The access token authenticates requests to
 * https://api.kimi.com/coding as an `Authorization: Bearer` header.
 */
import { getProviderEnvValue } from "../../utils/provider-env.js";
import { pollOAuthDeviceCodeFlow } from "./device-code.js";
const CLIENT_ID = "17e5f671-d194-4dfb-9706-5516cb48c098";
const DEFAULT_OAUTH_HOST = "https://auth.kimi.com";
const DEVICE_CODE_TIMEOUT_SECONDS = 15 * 60;
const DEFAULT_POLL_INTERVAL_SECONDS = 5;
const REQUEST_TIMEOUT_MS = 30 * 1000;
const REFRESH_MAX_RETRIES = 3;
function getOauthHost() {
    const override = getProviderEnvValue("KIMI_CODE_OAUTH_HOST") || getProviderEnvValue("KIMI_OAUTH_HOST");
    return (override || DEFAULT_OAUTH_HOST).replace(/\/+$/, "");
}
function requestSignal(signal) {
    return AbortSignal.any([AbortSignal.timeout(REQUEST_TIMEOUT_MS), ...(signal ? [signal] : [])]);
}
function formUrlEncode(fields) {
    return new URLSearchParams(fields).toString();
}
async function readJson(response) {
    try {
        const json = await response.json();
        return json && typeof json === "object" ? json : null;
    }
    catch {
        return null;
    }
}
/** The verification URI is opened in the user's browser; only http(s) URLs are trusted. */
function trustedHttpUrl(value) {
    if (typeof value !== "string" || !value)
        return null;
    try {
        const url = new URL(value);
        if (url.protocol !== "https:" && url.protocol !== "http:")
            return null;
        return url.href;
    }
    catch {
        return null;
    }
}
async function startDeviceAuthorization(oauthHost, signal) {
    const response = await fetch(`${oauthHost}/api/oauth/device_authorization`, {
        method: "POST",
        headers: {
            "Content-Type": "application/x-www-form-urlencoded",
            Accept: "application/json",
        },
        body: formUrlEncode({ client_id: CLIENT_ID }),
        signal: requestSignal(signal),
    });
    if (!response.ok) {
        const text = await response.text().catch(() => "");
        throw new Error(`Kimi Code device authorization failed with status ${response.status}${text ? `: ${text}` : ""}`);
    }
    const json = await readJson(response);
    const deviceCode = json?.device_code;
    const userCode = json?.user_code;
    const verificationUri = json?.verification_uri;
    const verificationUriComplete = json?.verification_uri_complete;
    if (typeof deviceCode !== "string" ||
        typeof userCode !== "string" ||
        typeof verificationUri !== "string" ||
        typeof verificationUriComplete !== "string" ||
        !trustedHttpUrl(verificationUriComplete) ||
        !trustedHttpUrl(verificationUri)) {
        throw new Error(`Invalid Kimi Code device authorization response: ${JSON.stringify(json)}`);
    }
    const interval = json?.interval;
    const expiresIn = json?.expires_in;
    return {
        deviceCode,
        userCode,
        verificationUri,
        verificationUriComplete,
        intervalSeconds: typeof interval === "number" && Number.isFinite(interval) && interval > 0
            ? interval
            : DEFAULT_POLL_INTERVAL_SECONDS,
        expiresInSeconds: typeof expiresIn === "number" && Number.isFinite(expiresIn) && expiresIn > 0
            ? expiresIn
            : DEVICE_CODE_TIMEOUT_SECONDS,
    };
}
function parseTokenResponse(json, operation) {
    const accessToken = json?.access_token;
    const refreshToken = json?.refresh_token;
    const expiresIn = json?.expires_in;
    if (typeof accessToken !== "string" ||
        !accessToken ||
        typeof refreshToken !== "string" ||
        !refreshToken ||
        typeof expiresIn !== "number" ||
        !Number.isFinite(expiresIn) ||
        expiresIn <= 0) {
        throw new Error(`Kimi Code token ${operation} response missing fields: ${JSON.stringify(json)}`);
    }
    return {
        access: accessToken,
        refresh: refreshToken,
        expires: Date.now() + expiresIn * 1000,
    };
}
async function pollForToken(oauthHost, device, signal) {
    return pollOAuthDeviceCodeFlow({
        intervalSeconds: device.intervalSeconds,
        expiresInSeconds: device.expiresInSeconds,
        waitBeforeFirstPoll: true,
        signal,
        poll: async () => {
            const response = await fetch(`${oauthHost}/api/oauth/token`, {
                method: "POST",
                headers: {
                    "Content-Type": "application/x-www-form-urlencoded",
                    Accept: "application/json",
                },
                body: formUrlEncode({
                    client_id: CLIENT_ID,
                    device_code: device.deviceCode,
                    grant_type: "urn:ietf:params:oauth:grant-type:device_code",
                }),
                signal: requestSignal(signal),
            });
            if (response.status >= 500) {
                const text = await response.text().catch(() => "");
                return {
                    status: "failed",
                    message: `Kimi Code device token request failed with status ${response.status}${text ? `: ${text}` : ""}`,
                };
            }
            const json = await readJson(response);
            if (response.ok && typeof json?.access_token === "string") {
                try {
                    return { status: "complete", value: parseTokenResponse(json, "poll") };
                }
                catch (error) {
                    return { status: "failed", message: error instanceof Error ? error.message : String(error) };
                }
            }
            const error = json?.error;
            const description = typeof json?.error_description === "string" ? `: ${json.error_description}` : "";
            if (error === "authorization_pending") {
                return { status: "pending" };
            }
            if (error === "slow_down") {
                const interval = json?.interval;
                return {
                    status: "slow_down",
                    intervalSeconds: typeof interval === "number" && interval > 0 ? interval : undefined,
                };
            }
            if (error === "expired_token") {
                return { status: "failed", message: "Kimi Code device authorization expired. Please restart login." };
            }
            if (error === "access_denied") {
                return { status: "failed", message: "Kimi Code login was denied." };
            }
            return {
                status: "failed",
                message: `Kimi Code device token request failed (status ${response.status})${typeof error === "string" ? `: ${error}${description}` : ""}`,
            };
        },
    });
}
function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
function isRetryableRefreshFailure(response) {
    return response.status === 429 || response.status >= 500;
}
async function refreshToken(oauthHost, refreshTokenValue, signal) {
    let lastError;
    for (let attempt = 0; attempt <= REFRESH_MAX_RETRIES; attempt++) {
        if (attempt > 0) {
            await sleep(1000 * 2 ** (attempt - 1));
        }
        if (signal?.aborted) {
            throw new Error("Kimi Code token refresh aborted");
        }
        let response;
        try {
            response = await fetch(`${oauthHost}/api/oauth/token`, {
                method: "POST",
                headers: {
                    "Content-Type": "application/x-www-form-urlencoded",
                    Accept: "application/json",
                },
                body: formUrlEncode({
                    client_id: CLIENT_ID,
                    grant_type: "refresh_token",
                    refresh_token: refreshTokenValue,
                }),
                signal: requestSignal(signal),
            });
        }
        catch (error) {
            lastError = error instanceof Error ? error : new Error(String(error));
            continue;
        }
        const json = await readJson(response);
        if (response.ok) {
            return parseTokenResponse(json, "refresh");
        }
        // Unauthorized: the stored credential is dead; Models clears it and prompts re-login.
        if (response.status === 401 || response.status === 403 || json?.error === "invalid_grant") {
            const description = typeof json?.error_description === "string" ? `: ${json.error_description}` : "";
            throw new Error(`Kimi Code token refresh unauthorized (status ${response.status})${description}`);
        }
        if (isRetryableRefreshFailure(response) && attempt < REFRESH_MAX_RETRIES) {
            lastError = new Error(`Kimi Code token refresh failed with status ${response.status}`);
            continue;
        }
        const text = JSON.stringify(json);
        throw new Error(`Kimi Code token refresh failed with status ${response.status}${text ? `: ${text}` : ""}`);
    }
    throw lastError ?? new Error("Kimi Code token refresh failed");
}
async function loginKimiCoding(interaction) {
    const oauthHost = getOauthHost();
    const device = await startDeviceAuthorization(oauthHost, interaction.signal);
    interaction.notify({
        type: "device_code",
        userCode: device.userCode,
        verificationUri: device.verificationUriComplete,
        intervalSeconds: device.intervalSeconds,
        expiresInSeconds: device.expiresInSeconds,
    });
    const token = await pollForToken(oauthHost, device, interaction.signal);
    return { type: "oauth", access: token.access, refresh: token.refresh, expires: token.expires };
}
export const kimiCodingOAuth = {
    name: "Kimi Code (subscription)",
    loginLabel: "Sign in with Kimi Code",
    login: loginKimiCoding,
    refresh: async (credential, signal) => {
        const token = await refreshToken(getOauthHost(), credential.refresh, signal);
        return { type: "oauth", access: token.access, refresh: token.refresh, expires: token.expires };
    },
    async toAuth(credential) {
        return { headers: { Authorization: `Bearer ${credential.access}` } };
    },
};
//# sourceMappingURL=kimi-coding.js.map