UNPKG

comfyui-mcp

Version:

Local-first, agent-native control plane for ComfyUI — MCP server + autonomous sidebar agent that drives your live graph in natural language on ANY LLM: Claude/ChatGPT/Gemini on your subscription (no API key), free local models via Ollama (fully offline),

92 lines • 5.12 kB
// oauth-bridge.ts — panel↔orchestrator bridge handlers for in-panel OAuth: // oauth_begin / oauth_status / oauth_signout. Extracted as pure, deps-injected // functions so they're unit-testable without a live bridge/socket — index.ts's // onPanelMessage dispatch just forwards the matching control frames here and // ships the reply back over the bridge (see docs/superpowers/specs/2026-07-11- // oauth-in-panel-design.md). // // SECURITY: every reply here is STATUS ONLY — provider id, account_label, mode, // user_code, verification_url. Access/refresh/id tokens never leave oauth-flow.ts // / code-provider-auth.ts, and never flow through these handlers or their // return values. Copilot (`experimental: true` in OAUTH_PROVIDERS) is refused // unless the caller explicitly passes allow_experimental:true — the panel only // sets that from its experimental row, never by default. import { ValidationError } from "../utils/errors.js"; import { logger } from "../utils/logger.js"; import { OAUTH_PROVIDERS, runLoopbackPKCE, beginDeviceCode, pollDeviceToken, } from "../services/oauth-flow.js"; import { persistOAuthResult, readOAuthStatus, clearOAuth, } from "../services/code-provider-auth.js"; function resolveProvider(providerId, deps) { const id = typeof providerId === "string" ? providerId.trim().toLowerCase() : ""; const providers = deps.providers ?? OAUTH_PROVIDERS; const cfg = id ? providers[id] : undefined; if (!cfg) { throw new ValidationError(`Unknown OAuth provider "${String(providerId ?? "")}". Known providers: ${Object.keys(providers).join(", ")}.`); } return cfg; } /** * `oauth_begin {provider, allow_experimental?}` — starts sign-in for `provider`. * * - loopback providers (codex, grok): returns immediately with * `{mode:"loopback", opened:true}`; the browser round-trip + token exchange * run in the BACKGROUND (never blocking the bridge socket) and, on success, * persist the result then call `onAuthChanged`. * - device providers (copilot): awaits the device-code request (fast — one * HTTP round trip) and returns `{mode:"device", user_code, * verification_url}` for the panel to display, then polls for completion in * the background the same way. * * copilot (and any future `experimental:true` provider) is REFUSED unless * `allow_experimental === true`. */ export async function handleOAuthBegin(args, deps = {}) { const cfg = resolveProvider(args.provider, deps); if (cfg.experimental && args.allow_experimental !== true) { throw new ValidationError(`${cfg.label} sign-in is experimental (ToS risk) — refused unless explicitly enabled from the experimental row.`); } const doLoopback = deps.runLoopbackPKCE ?? runLoopbackPKCE; const doBeginDevice = deps.beginDeviceCode ?? beginDeviceCode; const doPollDevice = deps.pollDeviceToken ?? pollDeviceToken; const doPersist = deps.persistOAuthResult ?? persistOAuthResult; const flowDeps = deps.oauthFlowDeps ?? {}; const authDeps = deps.codeProviderAuthDeps ?? {}; const onFlowError = (err) => { const message = err instanceof Error ? err.message : String(err); logger.warn(`[oauth-bridge] ${cfg.id} sign-in failed: ${message}`); deps.onBackgroundError?.(cfg.id, message); }; if (cfg.kind === "loopback_pkce") { // Fire-and-forget: the actual sign-in completes in the browser the flow // opens, entirely out-of-band from this reply. void doLoopback(cfg, flowDeps) .then((tokens) => doPersist(cfg.id, tokens, authDeps)) .then(() => deps.onAuthChanged?.(cfg.id)) .catch(onFlowError); return { provider: cfg.id, mode: "loopback", opened: true }; } // device_code const { user_code, verification_url, device_code } = await doBeginDevice(cfg, flowDeps); void doPollDevice(cfg, device_code, flowDeps) .then((tokens) => doPersist(cfg.id, tokens, authDeps)) .then(() => deps.onAuthChanged?.(cfg.id)) .catch(onFlowError); return { provider: cfg.id, mode: "device", user_code, verification_url }; } /** `oauth_status {}` — the status-only mirror (provider/account_label/timestamps) * for every provider ever signed into via the panel. Never token material. */ export function handleOAuthStatus(_args, deps = {}) { const doRead = deps.readOAuthStatus ?? readOAuthStatus; return { providers: doRead() }; } /** `oauth_signout {provider}` — clears the native token file (best-effort) and * the status mirror entry, then notifies `onAuthChanged` so readiness refreshes. */ export function handleOAuthSignout(args, deps = {}) { const cfg = resolveProvider(args.provider, deps); const doClear = deps.clearOAuth ?? clearOAuth; doClear(cfg.id, deps.codeProviderAuthDeps ?? {}); deps.onAuthChanged?.(cfg.id); // `provider` is echoed so the panel routes this ack to the right row (see // OAuthBeginResult.provider) — a signout can overlap a begin on another row. return { ok: true, provider: cfg.id }; } //# sourceMappingURL=oauth-bridge.js.map