openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
277 lines (276 loc) • 11.9 kB
JavaScript
import { C as parseStrictNonNegativeInteger } from "./number-coercion-CLj0HTDM.js";
import { t as pruneMapToMaxSize } from "./map-size-CNcWiFKu.js";
import { t as parseRetryAfterHeaderSeconds } from "./retry-after-BIGpiFES.js";
import "./http-body-D3IMwTJJ.js";
import { i as readResponseWithLimit } from "./http-response-body-CwT_cCNz.js";
import { o as getRuntimeConfigSnapshot } from "./runtime-snapshot-BaQikjTR.js";
import { d as isTrustedSecretSurfaceUnavailableError, n as SecretSurfaceUnavailableError, r as assertSecretOwnerAvailable } from "./runtime-degraded-state-D5EZZ925.js";
import { createHash } from "node:crypto";
//#region src/gateway/control-ui-github-api.ts
const GITHUB_API_ORIGIN = "https://api.github.com";
const CONTROL_UI_GITHUB_CREDENTIAL_UNAVAILABLE_MESSAGE = "The configured Control UI GitHub credential is unavailable. Resolve gateway.controlUi.github.token and retry.";
const GITHUB_JSON_MAX_BYTES = 262144;
const GITHUB_REQUEST_TIMEOUT_MS = 8e3;
const GITHUB_API_VERSION = "2022-11-28";
const GITHUB_API_MAX_REDIRECTS = 3;
const GITHUB_QUOTA_CACHE_LIMIT = 200;
const GITHUB_QUOTA_RETRY_MS = 6e4;
const transportCooldowns = /* @__PURE__ */ new WeakMap();
var ControlUiGitHubError = class extends Error {
constructor(statusCode, message, options = {}) {
super(message);
this.statusCode = statusCode;
this.name = "ControlUiGitHubError";
this.upstreamStatus = options.upstreamStatus ?? statusCode;
this.retryAtMs = options.retryAtMs;
this.retryable = options.retryable ?? (statusCode === 429 || options.upstreamStatus !== void 0 && options.upstreamStatus >= 500);
}
get retryAfterMs() {
return this.retryAtMs === void 0 ? void 0 : Math.max(0, this.retryAtMs - Date.now());
}
};
var ControlUiGitHubTransportError = class extends ControlUiGitHubError {
constructor(message) {
super(502, message, { retryable: true });
}
};
function formatControlUiGitHubPreviewError(error) {
if (isTrustedSecretSurfaceUnavailableError(error)) return {
message: CONTROL_UI_GITHUB_CREDENTIAL_UNAVAILABLE_MESSAGE,
retryable: false
};
if (error instanceof ControlUiGitHubTransportError) return {
message: `${error.message}. Retry or check GitHub availability.`,
retryable: true
};
if (error instanceof ControlUiGitHubError) {
const status = `HTTP ${error.upstreamStatus}`;
switch (error.statusCode) {
case 401: return {
message: `GitHub authentication failed (${status}). Reconnect the GitHub identity in Settings.`,
retryable: false
};
case 403: return {
message: `GitHub access denied (${status}). Check the configured GitHub identity's repository access.`,
retryable: false
};
case 404: return {
message: "GitHub item is unavailable or not public (HTTP 404). Open the link on GitHub to check access.",
retryable: false
};
case 429: {
const retryAfterMs = error.retryAfterMs;
return {
message: `GitHub API rate limit exceeded (${status}). ${retryAfterMs === void 0 ? "Wait" : `Wait ${Math.ceil(retryAfterMs / 1e3)} seconds`} and retry.`,
retryable: true,
...retryAfterMs === void 0 ? {} : { retryAfterMs }
};
}
case 502: return {
message: `${error.message.slice(0, 256)}. Retry or check GitHub availability.`,
retryable: true
};
}
}
if (error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError")) return {
message: "GitHub request timed out. Retry shortly.",
retryable: true
};
return {
message: "GitHub preview could not be loaded. Retry or check the server logs.",
retryable: false
};
}
function githubApiToken(env = process.env, config = getRuntimeConfigSnapshot()) {
const configured = config?.gateway?.controlUi?.github?.token;
if (configured !== void 0) {
assertSecretOwnerAvailable("capability", "control-ui-github");
const token = typeof configured === "string" ? configured.trim() : "";
if (!token) throw new SecretSurfaceUnavailableError({
ownerKind: "capability",
ownerId: "control-ui-github",
state: "unavailable",
paths: ["gateway.controlUi.github.token"],
refKeys: [],
reason: "secret reference was not materialized by the active runtime"
});
return token;
}
return env.GH_TOKEN?.trim() || env.GITHUB_TOKEN?.trim() || void 0;
}
/** Raw-config inspection for doctor; it never consults process-global runtime degradation state. */
function hasConfiguredGitHubApiCredential(env, config) {
return config.gateway?.controlUi?.github?.token !== void 0 || Boolean(env.GH_TOKEN?.trim() || env.GITHUB_TOKEN?.trim());
}
/** Captures the effective token and a non-secret cache scope from the same env snapshot. */
function resolveGitHubApiCredentialScope(env = process.env) {
const token = githubApiToken(env);
return {
token,
cacheScope: githubApiCredentialCacheScope(token)
};
}
function githubApiCredentialCacheScope(token) {
return token ? createHash("sha256").update(token).digest("hex") : "anonymous";
}
function githubApiResource(url) {
return url.pathname === "/search/code" ? "code_search" : url.pathname.startsWith("/search/") ? "search" : "core";
}
function activeGitHubCooldown(cooldowns, key) {
const error = cooldowns.get(key);
if (error && (error.retryAfterMs ?? 0) <= 0) {
cooldowns.delete(key);
return;
}
return error;
}
function githubApiHeaders(token) {
const headers = {
Accept: "application/vnd.github+json",
"User-Agent": "OpenClaw-Control-UI",
"X-GitHub-Api-Version": GITHUB_API_VERSION
};
if (token) headers.Authorization = `Bearer ${token}`;
return headers;
}
function isGitHubApiRedirect(status) {
return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
}
function safeGitHubApiUrl(raw, base) {
try {
const url = new URL(raw, base);
if (url.origin !== "https://api.github.com" || url.username || url.password || url.port) return null;
return url;
} catch {
return null;
}
}
async function fetchGitHubApi(rawUrl, fetchImpl, token, beforeRedirect, identity, etag) {
const initialUrl = safeGitHubApiUrl(rawUrl);
if (!initialUrl) throw new ControlUiGitHubError(502, "Invalid GitHub API URL");
let url = initialUrl;
const credentialScope = githubApiCredentialCacheScope(token);
const cooldowns = transportCooldowns.get(fetchImpl) ?? /* @__PURE__ */ new Map();
transportCooldowns.set(fetchImpl, cooldowns);
const signal = AbortSignal.timeout(GITHUB_REQUEST_TIMEOUT_MS);
for (let redirects = 0;; redirects += 1) {
if (identity) {
await identity.revalidate();
identity.assertSelected();
}
const resource = githubApiResource(url);
const sharedCooldown = activeGitHubCooldown(cooldowns, `${credentialScope}:*`);
const resourceCooldown = activeGitHubCooldown(cooldowns, `${credentialScope}:${resource}`);
const cooldown = (sharedCooldown?.retryAfterMs ?? 0) > (resourceCooldown?.retryAfterMs ?? 0) ? sharedCooldown : resourceCooldown;
if (cooldown) throw cooldown;
let response;
try {
response = await fetchImpl(url.href, {
headers: {
...githubApiHeaders(token),
...etag ? { "If-None-Match": etag } : {}
},
redirect: "manual",
signal
});
} catch (error) {
throw new ControlUiGitHubTransportError(signal.aborted || error instanceof Error && error.name === "TimeoutError" ? "GitHub request timed out" : "Could not reach GitHub");
}
if (isGitHubRateLimitResponse(response)) {
const error = githubResponseError(response);
const key = `${credentialScope}:${response.headers.get("x-ratelimit-remaining") === "0" ? response.headers.get("x-ratelimit-resource") ?? resource : "*"}`;
const previous = activeGitHubCooldown(cooldowns, key);
const retained = previous && (previous.retryAfterMs ?? 0) > (error.retryAfterMs ?? 0) ? previous : error;
cooldowns.set(key, retained);
pruneMapToMaxSize(cooldowns, GITHUB_QUOTA_CACHE_LIMIT);
await discardResponse(response);
throw retained;
}
if (!isGitHubApiRedirect(response.status)) return response;
const location = response.headers.get("location");
const nextUrl = location ? safeGitHubApiUrl(location, url) : null;
if (!nextUrl || redirects >= GITHUB_API_MAX_REDIRECTS) {
await discardResponse(response);
throw new ControlUiGitHubError(502, "GitHub API returned an unsafe redirect");
}
await discardResponse(response);
await beforeRedirect?.(nextUrl);
url = nextUrl;
}
}
async function discardResponse(response) {
await response.body?.cancel().catch(() => {});
}
async function readBoundedResponse(response, maxBytes) {
try {
return await readResponseWithLimit(response, maxBytes, { onOverflow: () => new ControlUiGitHubError(502, "GitHub response exceeded the size limit") });
} finally {
await discardResponse(response);
}
}
function isGitHubRateLimitResponse(response) {
if (response.status === 429) return true;
return response.status === 403 && (response.headers.get("x-ratelimit-remaining") === "0" || response.headers.has("retry-after"));
}
function githubResponseErrorStatus(response) {
if (isGitHubRateLimitResponse(response)) return 429;
if (response.status === 401 || response.status === 403 || response.status === 404) return response.status;
return 502;
}
function githubResponseError(response) {
const status = githubResponseErrorStatus(response);
let retryAtMs;
if (status === 429) {
const now = Date.now();
const retrySeconds = parseRetryAfterHeaderSeconds(response.headers.get("retry-after"));
const reset = response.headers.get("x-ratelimit-remaining") === "0" ? parseStrictNonNegativeInteger(response.headers.get("x-ratelimit-reset")) : void 0;
const proposed = retrySeconds !== void 0 ? now + retrySeconds * 1e3 : reset !== void 0 && reset <= Number.MAX_SAFE_INTEGER / 1e3 ? reset * 1e3 : void 0;
retryAtMs = proposed !== void 0 && Number.isSafeInteger(proposed) && proposed > now ? proposed : now + GITHUB_QUOTA_RETRY_MS;
}
return new ControlUiGitHubError(status, `GitHub request failed (HTTP ${response.status})`, {
upstreamStatus: response.status,
retryAtMs
});
}
async function withOptionalGitHubAuth(token, request) {
try {
return await request(token);
} catch (error) {
const status = error instanceof ControlUiGitHubError ? error.statusCode : 0;
if (token && [
401,
403,
429
].includes(status)) try {
return await request(void 0);
} catch (anonymousError) {
if (error instanceof ControlUiGitHubError && error.statusCode === 429 && anonymousError instanceof ControlUiGitHubError && anonymousError.statusCode === 429 && (error.retryAfterMs ?? Infinity) < (anonymousError.retryAfterMs ?? Infinity)) throw error;
throw anonymousError;
}
throw error;
}
}
async function readGitHubJsonResponse(response, maxBytes = GITHUB_JSON_MAX_BYTES) {
if (!response.ok) {
await discardResponse(response);
throw githubResponseError(response);
}
let body;
try {
body = await readBoundedResponse(response, maxBytes);
} catch (error) {
if (error instanceof ControlUiGitHubError) throw error;
throw new ControlUiGitHubError(502, "GitHub response could not be read");
}
try {
return JSON.parse(body.toString("utf8"));
} catch {
throw new ControlUiGitHubError(502, "GitHub response was not valid JSON");
}
}
/** Fetch a GitHub API JSON document with bounded size and normalized errors. */
function fetchGitHubJson(rawUrl, fetchImpl, token, maxBytes) {
return withOptionalGitHubAuth(token, async (requestToken) => readGitHubJsonResponse(await fetchGitHubApi(rawUrl, fetchImpl, requestToken), maxBytes));
}
//#endregion
export { discardResponse as a, formatControlUiGitHubPreviewError as c, hasConfiguredGitHubApiCredential as d, readBoundedResponse as f, withOptionalGitHubAuth as h, GITHUB_REQUEST_TIMEOUT_MS as i, githubApiCredentialCacheScope as l, resolveGitHubApiCredentialScope as m, ControlUiGitHubError as n, fetchGitHubApi as o, readGitHubJsonResponse as p, GITHUB_API_ORIGIN as r, fetchGitHubJson as s, CONTROL_UI_GITHUB_CREDENTIAL_UNAVAILABLE_MESSAGE as t, githubApiToken as u };