UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

277 lines (276 loc) 11.9 kB
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 };