openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
415 lines (414 loc) • 18 kB
JavaScript
import { s as asFiniteNumber } from "./number-coercion-CLj0HTDM.js";
import { c as isRecord } from "./record-coerce-DItp3I4t.js";
import { l as normalizeOptionalString, m as readNonBlankString } from "./string-coerce-CIXf7egm.js";
import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.js";
import { h as redactToolPayloadText } from "./redact-BtvPPfTi.js";
import "./utils-P__uGsPB.js";
import { t as pruneMapToMaxSize } from "./map-size-CNcWiFKu.js";
import { t as ErrorCodes } from "./gateway-error-details-w0nAGBBp.js";
import { d as errorShape } from "./error-codes-Bo8q2D1o.js";
import { s as buildGatewaySessionRow } from "./session-utils-list-B0k8KJn5.js";
import { n as resolveRequestedSessionAgentId } from "./session-request-agent-CCRSEGCB.js";
import "./session-utils-Cai0_C6U.js";
import { a as getActiveSecretsRuntimeConfigSnapshot } from "./runtime-state-C4aJ8Hzz.js";
import { c as prepareGitHubReadIdentity, n as GitHubIdentityError, p as resolveConfiguredGitHubToolIdentity } from "./github-tool-identity-CsNyDjEx.js";
import { a as discardResponse, c as formatControlUiGitHubPreviewError, f as readBoundedResponse, h as withOptionalGitHubAuth, i as GITHUB_REQUEST_TIMEOUT_MS, m as resolveGitHubApiCredentialScope, n as ControlUiGitHubError, o as fetchGitHubApi, p as readGitHubJsonResponse, r as GITHUB_API_ORIGIN } from "./control-ui-github-api-CCpVJnCA.js";
import { r as createSessionListEntryFilter } from "./session-sharing-B7MI8hNo.js";
import { t as resolveAgentIdOrRespondError } from "./agent-id-shared-MLgRqPvR.js";
import { r as requestCurrentGitHubOAuthRefresh } from "./github-oauth-lifecycle-DMB5peav.js";
import { a as loadSessionEntriesForTarget } from "./sessions-shared-D_lV07q2.js";
import { n as parseControlUiSessionPullRequestsSubscribeParams } from "./control-ui-session-pr-subscriptions-B_EFG2hk.js";
//#region src/gateway/control-ui-github-preview.ts
const GITHUB_AVATAR_HOST = "avatars.githubusercontent.com";
const GITHUB_AVATAR_MAX_BYTES = 262144;
const GITHUB_COMMITS_PAGE_SIZE = 100;
const GITHUB_COMMITS_MAX_BYTES = 1048576;
const CO_AUTHOR_FACE_LIMIT = 3;
const CO_AUTHOR_TRAILER = /^co-authored-by:\s*[^<]*<(?<id>\d{1,12})\+(?<login>[a-z\d](?:[a-z\d-]{0,38}))@users\.noreply\.github\.com>\s*$/gimu;
const AUTHENTICATED_SUCCESS_CACHE_MS = 3e5;
const ANONYMOUS_SUCCESS_CACHE_MS = 36e5;
const FAILURE_CACHE_MS = 3e4;
const CACHE_LIMIT = 200;
const previewCache = /* @__PURE__ */ new Map();
function isValidOwner(value) {
return /^(?=.{1,39}$)[a-z\d](?:[a-z\d-]*[a-z\d])?$/iu.test(value);
}
function isValidRepo(value) {
if (value.length < 1 || value.length > 100) return false;
const lower = value.toLowerCase();
if (!/^[a-z\d._-]+$/iu.test(value) || lower === "." || lower === "..") return false;
return !lower.endsWith(".git") && !lower.endsWith(".atom");
}
function parseControlUiGitHubPreviewTarget(value) {
if (!isRecord(value)) return null;
const kind = value.kind;
const owner = typeof value.owner === "string" ? value.owner.trim() : "";
const repo = typeof value.repo === "string" ? value.repo.trim() : "";
const number = value.number;
if (kind !== "issue" && kind !== "pull" || value.agentId !== void 0 && (typeof value.agentId !== "string" || !value.agentId.trim()) || !isValidOwner(owner) || !isValidRepo(repo) || typeof number !== "number" || !Number.isSafeInteger(number) || number < 1 || number > 9999999999) return null;
return {
kind,
number,
owner,
repo
};
}
function requiredString(record, key) {
const value = readNonBlankString(record[key]);
if (value === void 0) throw new ControlUiGitHubError(502, `GitHub response omitted ${key}`);
return value;
}
function redirectedRepositoryApiUrl(target, url) {
const segments = url.pathname.split("/").filter(Boolean);
const collection = target.kind === "pull" ? "pulls" : "issues";
const itemSegments = segments.at(-1) === "commits" ? segments.slice(0, -1) : segments;
if (itemSegments.length === 5 && itemSegments[0] === "repos" && itemSegments[1] && itemSegments[2] && itemSegments[3] === collection && /^\d+$/u.test(itemSegments[4] ?? "")) return `${GITHUB_API_ORIGIN}/repos/${itemSegments[1]}/${itemSegments[2]}`;
if (itemSegments.length === 4 && itemSegments[0] === "repositories" && /^\d+$/u.test(itemSegments[1] ?? "") && itemSegments[2] === collection && /^\d+$/u.test(itemSegments[3] ?? "")) return `${GITHUB_API_ORIGIN}/repositories/${itemSegments[1]}`;
return null;
}
function previewRepositoryApiUrl(target, value) {
if (target.kind === "issue") return requiredString(value, "repository_url");
const base = isRecord(value.base) ? value.base : {};
return requiredString(isRecord(base.repo) ? base.repo : {}, "url");
}
function parseGitHubResponse(target, value) {
const user = isRecord(value.user) ? value.user : {};
return {
preview: {
...target,
additions: asFiniteNumber(value.additions),
changedFiles: asFiniteNumber(value.changed_files),
closedAt: readNonBlankString(value.closed_at),
comments: asFiniteNumber(value.comments),
createdAt: requiredString(value, "created_at"),
deletions: asFiniteNumber(value.deletions),
draft: typeof value.draft === "boolean" ? value.draft : void 0,
login: readNonBlankString(user.login) ?? "ghost",
mergedAt: readNonBlankString(value.merged_at),
state: requiredString(value, "state"),
stateReason: readNonBlankString(value.state_reason),
title: requiredString(value, "title"),
updatedAt: requiredString(value, "updated_at")
},
avatarUrl: readNonBlankString(user.avatar_url)
};
}
function safeAvatarUrl(raw) {
if (!raw) return null;
try {
const url = new URL(raw);
const rawPathEnd = raw.search(/[?#]/u);
const rawPath = rawPathEnd === -1 ? raw : raw.slice(0, rawPathEnd);
if (url.protocol !== "https:" || url.hostname !== GITHUB_AVATAR_HOST || url.hash || url.username || url.password || url.port || rawPath.includes("..") || rawPath.includes("\\") || url.pathname.includes("..") || url.pathname.includes("\\")) return null;
url.search = "";
url.searchParams.set("s", "64");
return url;
} catch {
return null;
}
}
async function fetchCoAuthors(authorLogin, loadCommits, fetchImpl) {
const empty = {
coAuthors: [],
coAuthorCount: 0
};
let commits;
try {
commits = await loadCommits();
} catch {
return empty;
}
if (!Array.isArray(commits)) return empty;
const byLogin = /* @__PURE__ */ new Map();
for (const entry of commits) {
const commit = isRecord(entry) && isRecord(entry.commit) ? entry.commit : void 0;
const message = readNonBlankString(commit?.message);
if (!message) continue;
for (const match of message.matchAll(CO_AUTHOR_TRAILER)) {
const login = match.groups?.login;
const accountId = match.groups?.id;
if (!login || !accountId || login.toLowerCase() === authorLogin.toLowerCase()) continue;
const key = login.toLowerCase();
if (!byLogin.has(key)) byLogin.set(key, {
login,
accountId
});
}
}
const faces = [...byLogin.values()].slice(0, CO_AUTHOR_FACE_LIMIT);
return {
coAuthors: await Promise.all(faces.map(async (face) => {
const avatarDataUrl = await fetchAvatarDataUrl(`https://${GITHUB_AVATAR_HOST}/u/${face.accountId}`, fetchImpl);
return avatarDataUrl ? {
login: face.login,
avatarDataUrl
} : { login: face.login };
})),
coAuthorCount: byLogin.size
};
}
async function fetchAvatarDataUrl(rawUrl, fetchImpl) {
const url = safeAvatarUrl(rawUrl);
if (!url) return;
try {
const response = await fetchImpl(url, {
headers: { Accept: "image/webp,image/png,image/jpeg,image/gif" },
redirect: "error",
signal: AbortSignal.timeout(GITHUB_REQUEST_TIMEOUT_MS)
});
const contentType = response.headers.get("content-type")?.split(";", 1)[0]?.trim();
if (!response.ok || !contentType || ![
"image/gif",
"image/jpeg",
"image/png",
"image/webp"
].includes(contentType)) {
await discardResponse(response);
return;
}
return `data:${contentType};base64,${(await readBoundedResponse(response, GITHUB_AVATAR_MAX_BYTES)).toString("base64")}`;
} catch {
return;
}
}
async function fetchPreview(target, fetchImpl, token, identity) {
const request = (url, beforeRedirect) => fetchGitHubApi(url, fetchImpl, token, beforeRedirect, identity);
const assertPublicRepository = async (url) => {
const repository = await readGitHubJsonResponse(await request(url));
if (!isRecord(repository) || repository.private !== false) throw new ControlUiGitHubError(404, "GitHub repository is not public");
};
const repositoryUrl = `${GITHUB_API_ORIGIN}/repos/${encodeURIComponent(target.owner)}/${encodeURIComponent(target.repo)}`;
const itemUrl = `${repositoryUrl}/${target.kind === "pull" ? "pulls" : "issues"}/${target.number}`;
if (token) await assertPublicRepository(repositoryUrl);
const beforeRedirect = token ? async (url) => {
const redirectedRepositoryUrl = redirectedRepositoryApiUrl(target, url);
if (!redirectedRepositoryUrl) throw new ControlUiGitHubError(502, "GitHub item returned an unsafe redirect");
await assertPublicRepository(redirectedRepositoryUrl);
} : void 0;
const readItem = async (url, maxBytes) => readGitHubJsonResponse(await request(url, beforeRedirect), maxBytes);
const parsed = await readItem(itemUrl);
if (!isRecord(parsed)) throw new ControlUiGitHubError(502, "GitHub response was not an object");
if (token) await assertPublicRepository(previewRepositoryApiUrl(target, parsed));
const { preview, avatarUrl } = parseGitHubResponse(target, parsed);
const [avatarDataUrl, coAuthorFacts] = await Promise.all([fetchAvatarDataUrl(avatarUrl, fetchImpl), target.kind === "pull" ? fetchCoAuthors(preview.login, () => readItem(`${itemUrl}/commits?per_page=${GITHUB_COMMITS_PAGE_SIZE}`, GITHUB_COMMITS_MAX_BYTES), fetchImpl) : Promise.resolve({
coAuthors: [],
coAuthorCount: 0
})]);
return {
...preview,
...avatarDataUrl ? { avatarDataUrl } : {},
...coAuthorFacts.coAuthorCount > 0 ? {
coAuthors: coAuthorFacts.coAuthors,
coAuthorCount: coAuthorFacts.coAuthorCount
} : {}
};
}
function cacheKey(target, credentialScope) {
return `${target.kind}:${target.owner.toLowerCase()}/${target.repo.toLowerCase()}#${target.number}\0${credentialScope}`;
}
function cachePreview(key, entry) {
previewCache.set(key, entry);
pruneMapToMaxSize(previewCache, CACHE_LIMIT);
}
async function loadControlUiGitHubPreview(target, identity, fetchImpl = fetch) {
await identity?.revalidate();
identity?.assertSelected();
const { token, cacheScope } = identity ?? resolveGitHubApiCredentialScope();
const key = cacheKey(target, cacheScope);
const now = Date.now();
let entry = previewCache.get(key);
if (entry && entry.expiresAt <= now) {
previewCache.delete(key);
entry = void 0;
}
if (entry) {
previewCache.delete(key);
previewCache.set(key, entry);
} else {
const successCacheMs = token ? AUTHENTICATED_SUCCESS_CACHE_MS : ANONYMOUS_SUCCESS_CACHE_MS;
const request = identity ? fetchPreview(target, fetchImpl, token, identity) : withOptionalGitHubAuth(token, (requestToken) => fetchPreview(target, fetchImpl, requestToken));
const pending = {
expiresAt: now + successCacheMs,
promise: request.then((preview) => {
if (identity) cachePreview(key, pending);
return preview;
}, (error) => {
if (!(error instanceof GitHubIdentityError)) {
pending.expiresAt = Date.now() + FAILURE_CACHE_MS;
cachePreview(key, pending);
}
throw error;
})
};
entry = pending;
if (!identity) cachePreview(key, entry);
}
const preview = await entry.promise;
await identity?.revalidate();
identity?.assertSelected();
return preview;
}
//#endregion
//#region src/gateway/server-methods/control-ui.ts
async function prepareControlUiGitHubIdentity({ context, client, signal }, agentId) {
const config = context.getRuntimeConfig();
const configuredIdentity = () => {
const current = context.getRuntimeConfig();
return resolveConfiguredGitHubToolIdentity({
config: current,
agentId,
scope: "agent"
}) ?? resolveConfiguredGitHubToolIdentity({
config: current,
agentId,
scope: "system"
});
};
const assertActive = () => {
if (signal?.aborted || client?.connId && !context.getClientConnIds?.((current) => current === client).has(client.connId)) throw new GitHubIdentityError("changed");
};
const identity = configuredIdentity() ? await prepareGitHubReadIdentity({
config,
sourceConfig: getActiveSecretsRuntimeConfigSnapshot()?.sourceConfig ?? config,
agentId,
getCurrentConfig: () => context.getRuntimeConfig(),
assertActive,
refresh: () => requestCurrentGitHubOAuthRefresh(agentId)
}) : void 0;
return {
identity,
assertSelected: identity?.assertSelected ?? (() => {
assertActive();
if (configuredIdentity()) throw new GitHubIdentityError("changed");
})
};
}
const SESSION_PREVIEW_TEXT_MAX_CHARS = 200;
function boundedPreviewText(value, maxChars = SESSION_PREVIEW_TEXT_MAX_CHARS) {
const trimmed = value?.trim();
return trimmed ? truncateUtf16Safe(trimmed, maxChars) : void 0;
}
function parseSessionPreviewKey(params) {
if (!isRecord(params) || Object.keys(params).some((key) => key !== "sessionKey")) return null;
const sessionKey = typeof params.sessionKey === "string" ? params.sessionKey.trim() : "";
return sessionKey && sessionKey.length <= 512 ? sessionKey : null;
}
function projectSessionPreview(source) {
if (!source) return { status: "unavailable" };
const lastMessagePreview = boundedPreviewText(source.lastMessagePreview ? redactToolPayloadText(source.lastMessagePreview) : void 0);
const title = boundedPreviewText(source.title);
const derivedTitle = boundedPreviewText(source.derivedTitle);
const kind = boundedPreviewText(source.kind, 64);
const channel = boundedPreviewText(source.channel, 80);
return {
status: "ok",
sessionKey: source.sessionKey,
agentId: source.agentId,
...title ? { title } : {},
...derivedTitle ? { derivedTitle } : {},
...kind ? { kind } : {},
...channel ? { channel } : {},
...typeof source.updatedAt === "number" && Number.isFinite(source.updatedAt) ? { updatedAt: source.updatedAt } : {},
...lastMessagePreview ? { lastMessagePreview } : {},
...typeof source.archived === "boolean" ? { archived: source.archived } : {}
};
}
function loadControlUiSessionPreview(sessionKey, context, client) {
const cfg = context.getRuntimeConfig();
const requestedAgent = resolveRequestedSessionAgentId(cfg, sessionKey);
if (!requestedAgent.ok) return null;
const { target, storePath, store, entry } = loadSessionEntriesForTarget({
key: sessionKey,
cfg,
...requestedAgent.agentId ? { agentId: requestedAgent.agentId } : {}
});
if (!entry) return null;
const entryFilter = createSessionListEntryFilter({
client,
cfg
});
if (entryFilter && !entryFilter(target.canonicalKey, entry)) return null;
const row = buildGatewaySessionRow({
cfg,
storePath,
store,
key: target.canonicalKey,
entry,
includeDerivedTitles: true,
includeLastMessage: true,
skipTranscriptUsageFallback: true
});
return {
sessionKey: row.key,
agentId: row.agentId ?? target.agentId,
title: row.displayName,
derivedTitle: row.derivedTitle,
kind: row.kind,
channel: row.channel,
updatedAt: row.updatedAt,
lastMessagePreview: row.lastMessagePreview,
archived: row.archived
};
}
function createControlUiHandlers(loadGitHubPreview = loadControlUiGitHubPreview, loadSessionPreview = loadControlUiSessionPreview) {
return {
"controlUi.githubPreview": async (options) => {
const { params, respond, context } = options;
const target = parseControlUiGitHubPreviewTarget(params);
if (!target) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "invalid controlUi.githubPreview params"));
return;
}
const resolved = resolveAgentIdOrRespondError({
rawAgentId: params.agentId,
respond,
cfg: context.getRuntimeConfig(),
normalize: normalizeOptionalString
});
if (!resolved) return;
try {
const { identity, assertSelected } = await prepareControlUiGitHubIdentity(options, resolved.agentId);
const preview = await loadGitHubPreview(target, identity);
assertSelected();
respond(true, preview, void 0);
} catch (error) {
const { message, ...details } = error instanceof GitHubIdentityError ? {
message: error.message,
retryable: error.reason !== "unavailable"
} : formatControlUiGitHubPreviewError(error);
respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, message, details));
}
},
"controlUi.sessionPreview": async ({ params, client, context, respond }) => {
const sessionKey = parseSessionPreviewKey(params);
if (!sessionKey) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "invalid controlUi.sessionPreview params"));
return;
}
try {
respond(true, projectSessionPreview(await loadSessionPreview(sessionKey, context, client)), void 0);
} catch {
respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, "Session preview unavailable"));
}
},
"controlUi.sessionPullRequests.subscribe": ({ params, client, context, respond }) => {
const parsed = parseControlUiSessionPullRequestsSubscribeParams(params);
if (!parsed) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "invalid controlUi.sessionPullRequests.subscribe params"));
return;
}
const connId = client?.connId?.trim();
const subscriptions = context.controlUiSessionPullRequests;
if (!connId || !subscriptions) {
respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, "session pull request subscriptions unavailable"));
return;
}
if (parsed.refreshSessionKeys.length > 0) subscriptions.replace(connId, parsed.sessionKeys, new Set(parsed.refreshSessionKeys));
else subscriptions.replace(connId, parsed.sessionKeys);
respond(true, { subscribed: parsed.sessionKeys.length > 0 }, void 0);
}
};
}
const controlUiHandlers = createControlUiHandlers();
//#endregion
export { controlUiHandlers };