import crypto from "node:crypto";
import { McpOAuthStore } from "./oauth-store.js";
/**
 * Fixed loopback port for remote-MCP OAuth callbacks. It must stay STABLE across
 * logins: the redirect URI is baked into the dynamic client registration (RFC
 * 7591), so a changing port would invalidate the saved registration and force a
 * fresh one every time. Distinct from the provider-login port (1455).
 */
export const MCP_OAUTH_CALLBACK_PORT = 41999;
export const MCP_OAUTH_CALLBACK_PATH = "/oauth/callback";
/** The exact redirect URI registered with the authorization server. */
export function mcpOAuthRedirectUrl() {
    return `http://localhost:${MCP_OAUTH_CALLBACK_PORT}${MCP_OAUTH_CALLBACK_PATH}`;
}
/**
 * `OAuthClientProvider` for a single remote MCP server, backed by the on-disk
 * `McpOAuthStore`. The MCP SDK drives the whole RFC 6749/7591/8414 flow through
 * this object:
 *
 * - At connect time it reads `tokens()`; if absent/expired it tries a refresh,
 *   and if that fails it calls `redirectToAuthorization()` then throws
 *   `UnauthorizedError`.
 * - `redirectToAuthorization` is a NO-OP unless an `onRedirect` callback is
 *   supplied. Non-interactive paths (startup connect, add-probe) leave it unset,
 *   so a server needing auth fails cleanly (→ "requires login") without
 *   surprising the user with a browser tab. The interactive login path supplies
 *   `onRedirect` to open the browser.
 */
export class McpOAuthProvider {
    store;
    serverName;
    onRedirect;
    scope;
    constructor(opts) {
        this.serverName = opts.serverName;
        this.store = opts.store ?? new McpOAuthStore();
        this.onRedirect = opts.onRedirect;
        this.scope = opts.scope;
    }
    get redirectUrl() {
        return mcpOAuthRedirectUrl();
    }
    get clientMetadata() {
        return {
            client_name: "EZ Coder",
            client_uri: "https://github.com/kenkaiii/ezcoder",
            redirect_uris: [this.redirectUrl],
            grant_types: ["authorization_code", "refresh_token"],
            response_types: ["code"],
            token_endpoint_auth_method: "none",
            ...(this.scope ? { scope: this.scope } : {}),
        };
    }
    async state() {
        const entry = await this.store.get(this.serverName);
        if (entry.state)
            return entry.state;
        const fresh = crypto.randomBytes(16).toString("hex");
        await this.store.patch(this.serverName, { state: fresh });
        return fresh;
    }
    async clientInformation() {
        const entry = await this.store.get(this.serverName);
        return entry.clientInformation;
    }
    async saveClientInformation(info) {
        await this.store.patch(this.serverName, {
            clientInformation: info,
        });
    }
    async tokens() {
        const entry = await this.store.get(this.serverName);
        return entry.tokens;
    }
    async saveTokens(tokens) {
        await this.store.patch(this.serverName, { tokens });
    }
    redirectToAuthorization(authorizationUrl) {
        // Only interactive logins open a browser; otherwise this is intentionally a
        // no-op so the SDK throws UnauthorizedError and the caller marks the server
        // as "requires login" instead of popping a tab during a background connect.
        this.onRedirect?.(authorizationUrl);
    }
    async saveCodeVerifier(codeVerifier) {
        await this.store.patch(this.serverName, { codeVerifier });
    }
    async codeVerifier() {
        const entry = await this.store.get(this.serverName);
        if (!entry.codeVerifier) {
            throw new Error("No PKCE code verifier saved for this MCP server.");
        }
        return entry.codeVerifier;
    }
}
//# sourceMappingURL=oauth-provider.js.map