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
JavaScript
// 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