openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
92 lines (91 loc) • 4.24 kB
JavaScript
import { c as isRecord } from "./record-coerce-DItp3I4t.js";
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import { r as normalizeConfiguredMcpServers } from "./mcp-config-normalize-CWrDNtye.js";
import { c as readMcpOAuthStore, h as requesterMcpOAuthStoreKeyPrefix, s as readMcpOAuthPendingAuthorization } from "./mcp-oauth-store-rMpCI-eU.js";
import { a as completeOAuthCallback } from "./mcp-oauth-bxRFfdN1.js";
import { n as resolveMcpTransportConfig } from "./mcp-transport-config-mph-T0Ys.js";
//#region src/gateway/mcp-oauth-callback.ts
const MCP_OAUTH_CALLBACK_PATH = "/oauth/mcp/callback";
const MCP_OAUTH_CALLBACK_MAX_URL_BYTES = 8192;
const CONNECTED_HTML = "<!doctype html><html lang=\"en\"><meta charset=\"utf-8\"><title>Account connected</title><body><main><h1>You're connected.</h1><p>Return to the chat.</p></main></body></html>";
const RETRY_HTML = "<!doctype html><html lang=\"en\"><meta charset=\"utf-8\"><title>Sign-in incomplete</title><body><main><h1>Sign-in wasn't completed.</h1><p>Ask the bot to connect again.</p></main></body></html>";
const EXPIRED_HTML = "<!doctype html><html lang=\"en\"><meta charset=\"utf-8\"><title>Sign-in link expired</title><body><main><h1>This sign-in link expired or was already used.</h1><p>Ask the bot to connect again.</p></main></body></html>";
function respondHtml(res, status, body) {
res.statusCode = status;
res.setHeader("Cache-Control", "no-store");
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.end(body);
}
function readPendingState(lastAuthorizationUrl) {
try {
return new URL(lastAuthorizationUrl).searchParams.get("state")?.trim() || void 0;
} catch {
return;
}
}
function isPerRequesterServer(server) {
const oauth = isRecord(server.oauth) ? server.oauth : void 0;
return server.enabled !== false && server.auth === "oauth" && oauth?.identity === "per-requester";
}
/** Completes one requester MCP OAuth redirect using durable state correlation. */
async function handleMcpOAuthCallback(req, res, params) {
if (req.method !== "GET") return false;
const rawUrl = req.url ?? "/";
const url = new URL(rawUrl, "http://localhost");
if (url.pathname !== MCP_OAUTH_CALLBACK_PATH) return false;
const configuredServers = Object.entries(normalizeConfiguredMcpServers(params.config.mcp?.servers)).toSorted(([left], [right]) => left.localeCompare(right)).flatMap(([serverName, rawServer]) => {
if (!isPerRequesterServer(rawServer)) return [];
const resolved = resolveMcpTransportConfig(serverName, rawServer, { logWarnings: false });
return resolved?.kind === "http" && resolved.auth === "oauth" ? [{
serverName,
resolved
}] : [];
});
if (configuredServers.length === 0) return false;
if (Buffer.byteLength(rawUrl, "utf8") > MCP_OAUTH_CALLBACK_MAX_URL_BYTES) {
respondHtml(res, 400, RETRY_HTML);
return true;
}
const state = url.searchParams.get("state")?.trim();
const storeKey = state ? readMcpOAuthPendingAuthorization(state) : void 0;
const pending = storeKey ? readMcpOAuthStore(storeKey) : void 0;
if (!storeKey || !state || readPendingState(pending?.lastAuthorizationUrl ?? "") !== state) {
respondHtml(res, 404, EXPIRED_HTML);
return true;
}
const configuredServer = configuredServers.find(({ serverName, resolved }) => storeKey.startsWith(requesterMcpOAuthStoreKeyPrefix(serverName, resolved.url)));
if (!configuredServer) {
respondHtml(res, 404, EXPIRED_HTML);
return true;
}
if (url.searchParams.has("error")) {
respondHtml(res, 400, RETRY_HTML);
return true;
}
const code = url.searchParams.get("code")?.trim();
if (!code) {
respondHtml(res, 400, RETRY_HTML);
return true;
}
try {
if (await completeOAuthCallback({
storeKey,
principal: "requester",
serverName: configuredServer.serverName,
serverUrl: configuredServer.resolved.url
}, configuredServer.resolved, {
code,
state
}) === "expired") {
respondHtml(res, 404, EXPIRED_HTML);
return true;
}
respondHtml(res, 200, CONNECTED_HTML);
} catch (error) {
params.log.warn(`MCP OAuth callback failed for server "${configuredServer.serverName}": ${formatErrorMessage(error)}`);
respondHtml(res, 400, RETRY_HTML);
}
return true;
}
//#endregion
export { handleMcpOAuthCallback };