UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

678 lines (677 loc) • 30.4 kB
import { m as readNonBlankString } from "../../string-coerce-1VuiLaaR.mjs"; import { s as asFiniteNumber } from "../../number-coercion-CuVhdCBs.mjs"; import { c as isRecord } from "../../record-coerce-TIsquoVR.mjs"; import { r as truncateUtf16Safe } from "../../utf16-slice-D_ngcYKd.mjs"; import { t as pruneMapToMaxSize } from "../../map-size-BlrH_CnE.mjs"; import "../../string-coerce-runtime-50qKeWeL.mjs"; import { i as parseGitHubItemTarget, o as parseGitHubTarget, r as githubTargetUrl } from "../../targets-RjAEePy1.mjs"; import { S as withOptionalGitHubAuth, _ as readGitHubGraphQLResponse, a as GITHUB_API_ORIGIN, b as requiredString, c as GitHubGraphQLUnavailableError, d as fetchGitHubJson, f as formatControlUiGitHubPreviewError, g as readBoundedResponse, h as optionalNumber, i as GITHUB_API_BASE_URL, l as discardResponse, m as githubRestApiPath, n as githubPreviewView, o as GITHUB_GRAPHQL_URL, p as githubApiCredentialCacheScope, r as ControlUiGitHubError, s as GITHUB_REQUEST_TIMEOUT_MS, t as githubChangeMetadata, u as fetchGitHubApi, v as readGitHubJsonResponse, x as resolveGitHubApiUrls, y as readOptionalGitHubString } from "../../view-model-BP_nLgQX.mjs"; //#region extensions/github/src/preview.ts const GITHUB_AVATAR_HOST = "avatars.githubusercontent.com"; const GITHUB_AVATAR_MAX_BYTES = 262144; const GITHUB_PREVIEW_TIMEOUT_MS = 2e3; 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:[^<\r\n\u2028\u2029]*<(?<id>\d{1,12})\+(?<login>[a-z\d](?:[a-z\d-]{0,38}))@users\.noreply\.github\.com>[^\S\r\n\u2028\u2029]*$/gimu; const AUTHENTICATED_SUCCESS_CACHE_MS = 3e5; const ANONYMOUS_SUCCESS_CACHE_MS = 36e5; const FAILURE_CACHE_MS = 3e4; const CACHE_LIMIT$1 = 200; const previewCache = /* @__PURE__ */ new Map(); const parseControlUiGitHubPreviewTarget = parseGitHubItemTarget; function isPublicGitHubRepository(value) { return isRecord(value) && value.private === false && value.visibility === "public"; } async function assertPublicGitHubRepository(repositoryUrl, fetchImpl, token, identity, signal) { if (!isPublicGitHubRepository(await readGitHubJsonResponse(await fetchGitHubApi(repositoryUrl, fetchImpl, token, void 0, identity, void 0, signal, void 0, "https://api.github.com")))) throw new ControlUiGitHubError(404, "GitHub repository is not public"); } function redirectedRepositoryApiUrl(target, url) { const segments = githubRestApiPath(url, GITHUB_API_ORIGIN).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 : {}; const repository = isRecord(base.repo) ? base.repo : {}; return requiredString(repository, "url"); } function parseControlUiGitHubPreviewResponse(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; const url = URL.parse(raw); const rawPathEnd = raw.search(/[?#]/u); const rawPath = rawPathEnd === -1 ? raw : raw.slice(0, rawPathEnd); if (!url || 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; } async function fetchCoAuthors(authorLogin, loadCommits, fetchImpl, signal) { 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, signal); return avatarDataUrl ? { login: face.login, avatarDataUrl } : { login: face.login }; })), coAuthorCount: byLogin.size }; } async function fetchAvatarDataUrl(rawUrl, fetchImpl, signal) { 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 }); 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, signal, token, identity) { const request = (url, beforeRedirect) => fetchGitHubApi(url, fetchImpl, token, beforeRedirect, identity, void 0, signal, void 0, GITHUB_API_ORIGIN); const assertPublicRepository = (url) => assertPublicGitHubRepository(url, fetchImpl, token, identity, signal); 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 } = parseControlUiGitHubPreviewResponse(target, parsed); const [avatarDataUrl, coAuthorFacts] = await Promise.all([fetchAvatarDataUrl(avatarUrl, fetchImpl, signal), target.kind === "pull" ? fetchCoAuthors(preview.login, () => readItem(`${itemUrl}/commits?per_page=${GITHUB_COMMITS_PAGE_SIZE}`, GITHUB_COMMITS_MAX_BYTES), fetchImpl, signal) : 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$1); } async function loadControlUiGitHubPreview(target, identity, fetchImpl = fetch, refresh = false) { await identity?.revalidate(); identity?.assertSelected(); const { token, cacheScope } = identity ?? { token: void 0, cacheScope: "anonymous" }; const key = cacheKey(target, cacheScope); const now = Date.now(); let entry = previewCache.get(key); if (entry && (refresh || 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 signal = AbortSignal.timeout(GITHUB_PREVIEW_TIMEOUT_MS); const request = identity && !identity.optionalAuth ? fetchPreview(target, fetchImpl, signal, token, identity) : withOptionalGitHubAuth(token, (requestToken) => fetchPreview(target, fetchImpl, signal, requestToken, identity)); const pending = { expiresAt: now + successCacheMs, promise: request.then((preview) => { if (identity && !previewCache.has(key)) cachePreview(key, pending); return preview; }, (error) => { if (previewCache.get(key) === (identity ? void 0 : pending)) { if (error instanceof ControlUiGitHubError) { pending.expiresAt = Date.now() + FAILURE_CACHE_MS; cachePreview(key, pending); } else previewCache.delete(key); } throw error; }) }; entry = pending; if (!identity) cachePreview(key, entry); } const preview = await entry.promise; await identity?.revalidate(); identity?.assertSelected(); return preview; } //#endregion //#region extensions/github/src/detail-checks.ts const CHECK_LIMIT = 100; const LABEL_MAX_CHARS = 256; const STATE_ORDER = { failure: 0, pending: 1, success: 2, neutral: 3 }; const RUN_STATES = /* @__PURE__ */ new Map([ ["queued", { state: "pending", detail: "Queued" }], ["in_progress", { state: "pending", detail: "In progress" }], ["waiting", { state: "pending", detail: "Waiting" }], ["requested", { state: "pending", detail: "Waiting" }], ["pending", { state: "pending", detail: "Waiting" }] ]); const RUN_CONCLUSIONS = /* @__PURE__ */ new Map([ ["success", { state: "success", detail: "Passed" }], ["neutral", { state: "neutral", detail: "Neutral" }], ["skipped", { state: "neutral", detail: "Skipped" }], ["failure", { state: "failure", detail: "Failed" }], ["cancelled", { state: "failure", detail: "Canceled" }], ["timed_out", { state: "failure", detail: "Timed out" }], ["action_required", { state: "failure", detail: "Action required" }], ["stale", { state: "failure", detail: "Stale" }], ["startup_failure", { state: "failure", detail: "Could not start" }] ]); const STATUS_STATES = /* @__PURE__ */ new Map([ ["success", { state: "success", detail: "Passed" }], ["pending", { state: "pending", detail: "Pending" }], ["failure", { state: "failure", detail: "Failed" }], ["error", { state: "failure", detail: "Error" }] ]); function count(value) { if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new ControlUiGitHubError(502, "GitHub checks returned an invalid count"); return value; } function checkUrl(value) { if (typeof value !== "string" || value.length > 4096) return; const url = URL.parse(value); return url?.protocol === "https:" && !url.username && !url.password ? url.href : void 0; } function checkState(entry, kind) { const state = kind === "statuses" ? STATUS_STATES.get(entry.state) : entry.status === "completed" ? RUN_CONCLUSIONS.get(entry.conclusion) : RUN_STATES.get(entry.status); if (!state) throw new ControlUiGitHubError(502, kind === "runs" ? "GitHub check returned an unknown state" : "GitHub commit status returned an unknown state"); return state; } function parsePage(page, commit, kind) { if (!isRecord(page.value)) throw new ControlUiGitHubError(502, "GitHub checks were not an object"); const value = page.value; const entries = kind === "runs" ? value.check_runs : value.statuses; if (!Array.isArray(entries) || kind === "statuses" && value.sha !== commit) throw new ControlUiGitHubError(502, "GitHub checks returned a different revision or invalid list"); const reportedTotal = count(value.total_count); const latest = /* @__PURE__ */ new Map(); for (const entry of entries.slice(0, CHECK_LIMIT)) { if (!isRecord(entry) || kind === "runs" && entry.head_sha !== commit) throw new ControlUiGitHubError(502, "GitHub check returned a different revision or invalid item"); const id = count(entry.id); const name = requiredString(entry, kind === "runs" ? "name" : "context"); const key = kind === "runs" ? String(id) : name.toLowerCase(); if ((latest.get(key)?.id ?? -1) >= id) continue; latest.set(key, { id, item: { name: name.slice(0, LABEL_MAX_CHARS), ...checkState(entry, kind), url: checkUrl(kind === "runs" ? entry.html_url : entry.target_url) } }); } const missing = Math.max(0, reportedTotal - Math.min(entries.length, CHECK_LIMIT)); return { items: Array.from(latest.values(), ({ item }) => item), total: latest.size + missing, truncated: page.hasNextPage || missing > 0 || entries.length > CHECK_LIMIT }; } async function fetchPullChecks(repositoryUrl, headSha, url, fetchPage) { if (typeof headSha !== "string" || !/^[a-f0-9]{40}$/u.test(headSha)) return { state: "unavailable", summary: "Checks unavailable", total: 0, items: [], truncated: true, url }; const commitUrl = repositoryUrl + "/commits/" + headSha; const items = []; let total = 0; let truncated = false; for (const kind of ["runs", "statuses"]) try { const page = parsePage(await fetchPage(commitUrl + (kind === "runs" ? "/check-runs?filter=latest&per_page=" : "/status?per_page=") + CHECK_LIMIT), headSha, kind); items.push(...page.items); total += page.total; truncated ||= page.truncated; } catch { truncated = true; } items.sort((a, b) => STATE_ORDER[a.state] - STATE_ORDER[b.state] || a.name.localeCompare(b.name)); truncated ||= items.length > CHECK_LIMIT; const counts = { success: 0, failure: 0, pending: 0, neutral: 0 }; for (const item of items) counts[item.state] += 1; return { state: counts.failure ? "failure" : counts.pending ? "pending" : truncated ? "unavailable" : counts.success ? "success" : "neutral", summary: [ truncated ? items.length ? "Checks incomplete" : "Checks unavailable" : void 0, counts.failure ? counts.failure + " failed" : void 0, counts.pending ? counts.pending + " pending" : void 0, counts.success ? counts.success + " passed" : void 0, counts.neutral ? counts.neutral + " skipped or neutral" : void 0 ].filter(Boolean).join(" · ") || "No checks reported", total, items: items.slice(0, CHECK_LIMIT), truncated, url, commit: headSha }; } //#endregion //#region extensions/github/src/detail.ts const DETAIL_JSON_MAX_BYTES = 1048576; const BODY_MAX_CHARS = 32768; const COMMENT_LIMIT = 20; const COMMENT_MAX_CHARS = 4096; const FILE_LIMIT = 30; const PATCH_MAX_CHARS = 16384; const PATCH_TOTAL_MAX_CHARS = 98304; const SUCCESS_CACHE_MS = 3e5; const PARTIAL_CACHE_MS = 3e4; const CACHE_LIMIT = 32; const detailCache = /* @__PURE__ */ new Map(); var GitHubDetailAccessError = class extends ControlUiGitHubError { constructor() { super(404, "GitHub repository is not public or has changed"); } }; function redirectedRepositoryUrl(url, suffix) { const match = /^(\/repos\/[^/]+\/[^/]+|\/repositories\/\d+)(\/.*)?$/u.exec(githubRestApiPath(url, GITHUB_API_ORIGIN)); if (!match || (match[2] ?? "") !== suffix) throw new GitHubDetailAccessError(); return GITHUB_API_ORIGIN + match[1]; } async function readPublicRepository(url, fetchImpl, identity, expectedId) { const repository = await readGitHubJsonResponse(await fetchGitHubApi(url, fetchImpl, identity.token, async (redirect) => { redirectedRepositoryUrl(redirect, ""); }, identity, void 0, void 0, void 0, GITHUB_API_ORIGIN)); const id = isRecord(repository) ? optionalNumber(repository, "id") : void 0; if (!isPublicGitHubRepository(repository) || id === void 0 || !Number.isSafeInteger(id) || id <= 0 || expectedId !== void 0 && id !== expectedId) throw new GitHubDetailAccessError(); return id; } function markdownBody(value, maxChars) { const body = typeof value === "string" ? value : ""; return { body: truncateUtf16Safe(body, maxChars), bodyTruncated: body.length > maxChars }; } function requiredCount(value, key) { const count = optionalNumber(value, key); if (count === void 0 || !Number.isSafeInteger(count) || count < 0) throw new ControlUiGitHubError(502, `GitHub response omitted ${key}`); return count; } function commentPosition(value) { return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0; } function commentUrl(comment) { const url = new URL(requiredString(comment, "html_url")); if (url.origin !== "https://github.com" || url.username || url.password) throw new ControlUiGitHubError(502, "GitHub comment returned an unsafe permalink"); return url.href; } function parseComments(value, kind) { if (!Array.isArray(value)) throw new ControlUiGitHubError(502, "GitHub comments were not an array"); return value.slice(0, COMMENT_LIMIT).map((comment) => { if (!isRecord(comment)) throw new ControlUiGitHubError(502, "GitHub comment was not an object"); const user = isRecord(comment.user) ? comment.user : {}; const { body, bodyTruncated } = markdownBody(comment.body, COMMENT_MAX_CHARS); const diffHunk = kind === "review" ? markdownBody(comment.diff_hunk, COMMENT_MAX_CHARS) : void 0; const id = requiredCount(comment, "id"); const url = commentUrl(comment); const path = kind !== "discussion" ? readOptionalGitHubString(comment, "path") : void 0; const line = commentPosition(comment.line) ?? commentPosition(comment.original_line); const start = commentPosition(comment.start_line) ?? commentPosition(comment.original_start_line); const lineLabel = line === void 0 ? void 0 : (start !== void 0 && start !== line ? start + "–" : "") + line; const replyId = kind === "review" ? commentPosition(comment.in_reply_to_id) : void 0; const locationLabels = [comment.side === "LEFT" ? "Before change" : comment.side === "RIGHT" ? "After change" : void 0, comment.line == null && commentPosition(comment.original_line) ? "Outdated" : void 0].filter(Boolean); return { id: new URL(url).hash.slice(1) || (kind === "review" ? "discussion_r" : kind === "commit" ? "commitcomment-" : "issuecomment-") + id, url, author: readOptionalGitHubString(user, "login") ?? "ghost", createdAt: requiredString(comment, "created_at"), body, bodyTruncated, label: kind === "review" ? "Review comment" : void 0, context: kind !== "discussion" ? { path, lineLabel, label: locationLabels.join(" · ") || void 0, diff: diffHunk?.body, diffTruncated: kind === "review" ? typeof comment.diff_hunk !== "string" || diffHunk?.bodyTruncated : void 0, replyUrl: replyId === void 0 ? void 0 : new URL("#discussion_r" + replyId, url).href, replyLabel: replyId === void 0 ? void 0 : "In reply to #" + replyId } : void 0 }; }); } async function fetchComments(url, kind, total, readPage) { if (total === 0) return { comments: [], commentsTotal: 0, commentsTruncated: false }; try { const sort = kind === "review" ? "&sort=created&direction=asc" : ""; const page = await readPage(url + "?per_page=20" + sort); const comments = parseComments(page.value, kind); return { comments, commentsTotal: total, commentsTruncated: page.hasNextPage || total > comments.length || Array.isArray(page.value) && page.value.length > COMMENT_LIMIT }; } catch (error) { if (error instanceof GitHubDetailAccessError) throw error; return { comments: [], commentsTotal: total, commentsTruncated: true }; } } function parseFiles(value) { if (!Array.isArray(value)) throw new ControlUiGitHubError(502, "GitHub files were not an array"); let remainingPatchChars = PATCH_TOTAL_MAX_CHARS; return value.slice(0, FILE_LIMIT).map((file) => { if (!isRecord(file)) throw new ControlUiGitHubError(502, "GitHub file was not an object"); const rawPatch = typeof file.patch === "string" ? file.patch : void 0; const patch = rawPatch === void 0 ? void 0 : truncateUtf16Safe(rawPatch, Math.min(PATCH_MAX_CHARS, remainingPatchChars)); remainingPatchChars -= patch?.length ?? 0; return { path: requiredString(file, "filename"), previousPath: readOptionalGitHubString(file, "previous_filename"), status: requiredString(file, "status"), additions: requiredCount(file, "additions"), deletions: requiredCount(file, "deletions"), patch, patchTruncated: rawPatch === void 0 || patch?.length !== rawPatch.length }; }); } async function fetchDetail(target, readPage) { const repoPath = `/repos/${encodeURIComponent(target.owner)}/${encodeURIComponent(target.repo)}`; const repositoryUrl = GITHUB_API_ORIGIN + repoPath; const itemUrl = `${repositoryUrl}/${target.kind === "commit" ? "commits" : target.kind === "pull" ? "pulls" : "issues"}/${target.kind === "commit" ? target.sha : target.number}`; const itemPage = await readPage(target.kind === "commit" ? `${itemUrl}?per_page=${FILE_LIMIT}` : itemUrl); if (!isRecord(itemPage.value)) throw new ControlUiGitHubError(502, "GitHub response was not an object"); const value = itemPage.value; const url = githubTargetUrl(target); if (target.kind === "commit") { const commit = isRecord(value.commit) ? value.commit : {}; const author = isRecord(value.author) ? value.author : {}; const commitAuthor = isRecord(commit.author) ? commit.author : {}; const stats = isRecord(value.stats) ? value.stats : {}; const content = markdownBody(commit.message, BODY_MAX_CHARS); const discussion = await fetchComments(itemUrl + "/comments", "commit", requiredCount(commit, "comment_count"), readPage); const files = value.files === void 0 ? [] : parseFiles(value.files); const filesTruncated = value.files === void 0 || itemPage.hasNextPage || Array.isArray(value.files) && value.files.length > FILE_LIMIT; return { url, subtitle: target.owner + "/" + target.repo + " · " + requiredString(value, "sha").slice(0, 12), badge: { label: "Commit", tone: "neutral" }, title: content.body.split("\n", 1)[0] ?? "", author: readOptionalGitHubString(author, "login") ?? readOptionalGitHubString(commitAuthor, "name") ?? "ghost", createdAt: readOptionalGitHubString(commitAuthor, "date"), metadata: githubChangeMetadata(optionalNumber(stats, "additions"), optionalNumber(stats, "deletions")), ...content, ...discussion, files, filesTruncated, partial: content.bodyTruncated || discussion.commentsTruncated || filesTruncated || discussion.comments.some((comment) => comment.bodyTruncated) || files.some((file) => file.patchTruncated) }; } const { preview } = parseControlUiGitHubPreviewResponse(target, value); const content = markdownBody(value.body, BODY_MAX_CHARS); const discussion = await fetchComments(repositoryUrl + "/issues/" + target.number + "/comments", "discussion", requiredCount(value, "comments"), readPage); const review = target.kind === "pull" ? await fetchComments(itemUrl + "/comments", "review", requiredCount(value, "review_comments"), readPage) : void 0; const comments = discussion.comments.concat(review?.comments ?? []).toSorted((left, right) => (left.createdAt ?? "").localeCompare(right.createdAt ?? "") || left.id.localeCompare(right.id, "en", { numeric: true })); const commentsTotal = discussion.commentsTotal + (review?.commentsTotal ?? 0); const commentsTruncated = discussion.commentsTruncated || review?.commentsTruncated === true; let files = []; let filesTruncated = false; const filesTotal = target.kind === "pull" ? requiredCount(value, "changed_files") : 0; if (filesTotal > 0) try { const page = await readPage(`${itemUrl}/files?per_page=${FILE_LIMIT}`); files = parseFiles(page.value); filesTruncated = page.hasNextPage || filesTotal > files.length; } catch (error) { if (error instanceof GitHubDetailAccessError) throw error; filesTruncated = true; } const head = isRecord(value.head) ? value.head : {}; const base = isRecord(value.base) ? value.base : {}; const checks = target.kind === "pull" ? await fetchPullChecks(repositoryUrl, head.sha, url + "/checks", readPage) : void 0; const view = githubPreviewView(preview); const metadata = githubChangeMetadata(preview.additions, preview.deletions, preview.changedFiles, preview.comments); const headRef = readOptionalGitHubString(head, "ref")?.slice(0, 256); const baseRef = readOptionalGitHubString(base, "ref")?.slice(0, 256); return { ...view, metadata: target.kind === "pull" && headRef && baseRef ? [...metadata, { label: "Branch", value: headRef + " → " + baseRef }] : metadata, url, ...content, ...checks ? { checks } : {}, comments, commentsTotal, commentsTruncated, files, filesTotal, filesTruncated, partial: content.bodyTruncated || commentsTruncated || filesTruncated || checks?.truncated === true || checks?.state === "unavailable" || comments.some((comment) => comment.bodyTruncated || comment.context?.diffTruncated) || files.some((file) => file.patchTruncated) }; } async function loadGitHubDetailWithIdentity(target, identity, fetchImpl = fetch, refresh = false) { const parsed = parseGitHubTarget(target); if (!parsed) throw new ControlUiGitHubError(400, "Invalid GitHub detail target"); await identity?.revalidate(); identity?.assertSelected(); const id = parsed.kind === "commit" ? parsed.sha : parsed.number; const key = `${parsed.kind}:${parsed.owner.toLowerCase()}/${parsed.repo.toLowerCase()}#${id}\0${identity?.cacheScope ?? "anonymous"}\0${githubApiCredentialCacheScope(identity?.token)}`; const assertDelivery = async (result) => { if (identity?.token) for (const url of result.repositoryUrls) await readPublicRepository(url, fetchImpl, identity, result.repositoryId); await identity?.revalidate(); identity?.assertSelected(); }; const cached = detailCache.get(key); if (!refresh && cached && cached.expiresAt > Date.now() && (!identity || cached.settled)) { detailCache.delete(key); detailCache.set(key, cached); const result = await cached.promise; await assertDelivery(result); return result.document; } detailCache.delete(key); const load = async () => { const repositoryUrl = `${GITHUB_API_ORIGIN}/repos/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.repo)}`; const repositoryUrls = /* @__PURE__ */ new Set([repositoryUrl]); const repositoryId = identity?.token ? await readPublicRepository(repositoryUrl, fetchImpl, identity) : void 0; if (!identity?.token) await assertPublicGitHubRepository(repositoryUrl, fetchImpl, void 0, identity); const readPage = async (url) => { const suffix = new URL(url).pathname.slice(new URL(repositoryUrl).pathname.length); const response = await fetchGitHubApi(url, fetchImpl, identity?.token, identity?.token ? async (redirect) => { const redirected = redirectedRepositoryUrl(redirect, suffix); await readPublicRepository(redirected, fetchImpl, identity, repositoryId); repositoryUrls.add(redirected); } : void 0, identity, void 0, void 0, void 0, GITHUB_API_ORIGIN); return { hasNextPage: /;\s*rel="next"/u.test(response.headers.get("link") ?? ""), value: await readGitHubJsonResponse(response, DETAIL_JSON_MAX_BYTES) }; }; const result = { document: await fetchDetail(parsed, readPage), repositoryId, repositoryUrls: [...repositoryUrls] }; await assertDelivery(result); return result; }; const entry = { expiresAt: Date.now() + SUCCESS_CACHE_MS, settled: false, promise: load().then((result) => { entry.settled = true; if (parsed.kind === "pull" || result.document.partial) entry.expiresAt = Date.now() + PARTIAL_CACHE_MS; return result; }).catch((error) => { if (error instanceof ControlUiGitHubError && error.statusCode !== 409) { entry.settled = true; entry.expiresAt = Date.now() + PARTIAL_CACHE_MS; } else if (detailCache.get(key) === entry) detailCache.delete(key); throw error; }) }; detailCache.set(key, entry); pruneMapToMaxSize(detailCache, CACHE_LIMIT); const result = await entry.promise; identity?.assertSelected(); return result.document; } function loadGitHubDetail(target, identity, fetchImpl = fetch, refresh = false) { return identity?.optionalAuth ? withOptionalGitHubAuth(identity.token, (token) => loadGitHubDetailWithIdentity(target, { ...identity, token }, fetchImpl, refresh)) : loadGitHubDetailWithIdentity(target, identity, fetchImpl, refresh); } //#endregion export { ControlUiGitHubError, GITHUB_API_BASE_URL, GITHUB_API_ORIGIN, GITHUB_GRAPHQL_URL, GITHUB_REQUEST_TIMEOUT_MS, GitHubGraphQLUnavailableError, discardResponse, fetchGitHubApi, fetchGitHubJson, formatControlUiGitHubPreviewError, githubApiCredentialCacheScope, isRecord, loadControlUiGitHubPreview, loadGitHubDetail, optionalNumber, parseControlUiGitHubPreviewTarget, parseGitHubTarget, readBoundedResponse, readGitHubGraphQLResponse, readGitHubJsonResponse, readOptionalGitHubString, requiredString, resolveGitHubApiUrls, withOptionalGitHubAuth };