import { kcGet, kcSet, kcDelete, kcGetJson, kcSetJson } from "./keychain.js"; import { HOST, PORT } from "./config.js"; // When the SDK needs the user to authorize, it calls redirectToAuthorization() // with the provider URL. A headless bridge can't open a browser itself, so it // stashes the URL here keyed by serverId; POST /mcp/oauth/start returns it for // the UI to open. Cleared once consumed. const pendingAuthUrls = new Map(); export function takePendingAuthUrl(serverId) { const u = pendingAuthUrls.get(serverId); pendingAuthUrls.delete(serverId); return u; } // OAuthClientProvider whose entire persistent state lives in the macOS Keychain // (service ai.neuron.connect). One instance per serverId. Survives bridge // restarts: tokens, the dynamic-registration client info, and the in-flight PKCE // verifier are all keyed under :*. export class KeychainOAuthProvider { serverId; scope; constructor(serverId, scope) { this.serverId = serverId; this.scope = scope; } get redirectUrl() { return `http://${HOST}:${PORT}/mcp/oauth/callback?id=${encodeURIComponent(this.serverId)}`; } get clientMetadata() { return { client_name: `Neuron (${this.serverId})`, redirect_uris: [this.redirectUrl], grant_types: ["authorization_code", "refresh_token"], response_types: ["code"], token_endpoint_auth_method: "none", // public client + PKCE ...(this.scope ? { scope: this.scope } : {}), }; } // Carry the serverId through the round-trip so the callback knows which // connector to finish, even across a bridge restart. state() { return this.serverId; } async clientInformation() { return kcGetJson(`${this.serverId}:client`); } async saveClientInformation(info) { await kcSetJson(`${this.serverId}:client`, info); } async tokens() { return kcGetJson(`${this.serverId}:oauth`); } async saveTokens(tokens) { await kcSetJson(`${this.serverId}:oauth`, tokens); } async redirectToAuthorization(url) { pendingAuthUrls.set(this.serverId, url.toString()); } async saveCodeVerifier(verifier) { await kcSet(`${this.serverId}:verifier`, verifier); } async codeVerifier() { const v = await kcGet(`${this.serverId}:verifier`); if (!v) throw new Error(`no PKCE verifier stored for ${this.serverId}`); return v; } async invalidateCredentials(scope) { if (scope === "all" || scope === "tokens") await kcDelete(`${this.serverId}:oauth`); if (scope === "all" || scope === "client") await kcDelete(`${this.serverId}:client`); if (scope === "all" || scope === "verifier") await kcDelete(`${this.serverId}:verifier`); } // True once we hold an access token (used for status reporting). async hasTokens() { return (await this.tokens()) !== undefined; } }