openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
267 lines (266 loc) • 11.6 kB
JavaScript
import { a as asOptionalRecord } from "../../record-coerce-TIsquoVR.mjs";
import { _ as resolveSecretInputString } from "../../types.secrets-DD-Q2SS_.mjs";
import { r as createRuntimeConfigReader } from "../../runtime-snapshot-De_QxZI9.mjs";
import "../../string-coerce-runtime-BbIJhy7G.mjs";
import "../../secret-input-B0WNzmEZ.mjs";
import "../../runtime-config-snapshot--IkldAgP.mjs";
import { i as resolveXAccount, n as listXRuntimeAccountIds } from "../../accounts-DgZqMZQn.mjs";
import { i as openXAllowlist } from "../../allowlist-ZXKTf1Qk.mjs";
import { a as XBudgetExceededError, t as getXApi } from "../../client-DleoFeSj.mjs";
import { r as syncXGitHub, t as emptyXGitHubSnapshot } from "../../verified-github-BmprWpkw.mjs";
//#region extensions/x/src/github.ts
var XGitHubError = class extends Error {
constructor(message) {
super(message);
this.name = "XGitHubError";
}
};
const API_ORIGIN = "https://api.github.com";
const PERMISSION_RANK = {
push: 1,
maintain: 2,
admin: 3
};
const LOGIN_PATTERN = /^[a-z\d][a-z\d_-]*$/i;
function xHandle(value) {
if (typeof value !== "string") return;
let handle = value.trim();
if (/^https?:\/\//i.test(handle)) try {
const url = new URL(handle);
if (![
"x.com",
"twitter.com",
"www.x.com",
"www.twitter.com"
].includes(url.hostname) || url.username || url.password || url.port || url.search || url.hash) return;
handle = url.pathname.replace(/^\//, "").replace(/\/$/, "");
} catch {
return;
}
handle = handle.replace(/^@/, "");
return /^[a-z\d_]{1,15}$/i.test(handle) ? handle.toLowerCase() : void 0;
}
function invalidResponse() {
throw new XGitHubError("GitHub returned an invalid response; the last verified set is unchanged.");
}
function retryDeadline(response) {
const now = Date.now();
const retry = response.headers.get("retry-after");
const seconds = retry === null ? NaN : Number(retry);
const deadlines = [retry === null ? NaN : Number.isFinite(seconds) ? now + seconds * 1e3 : Date.parse(retry), Number(response.headers.get("x-ratelimit-reset")) * 1e3].filter((value) => Number.isFinite(value) && value > now);
return deadlines.length ? Math.max(...deadlines) : void 0;
}
function createXGitHubReader(options) {
const profiles = /* @__PURE__ */ new Map();
let retryAt = 0;
function rateLimited() {
throw new XGitHubError(`GitHub rate limit reached; retry after ${new Date(retryAt).toISOString()}.`);
}
async function request(path, parse, signal, cached) {
if (Date.now() < retryAt) rateLimited();
if (!options.token.trim()) throw new XGitHubError("Configure verifiedFromGitHub.token with a GitHub credential that can list repository collaborators.");
const requestSignal = signal ? AbortSignal.any([signal, AbortSignal.timeout(3e4)]) : AbortSignal.timeout(3e4);
const headers = {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${options.token}`,
"X-GitHub-Api-Version": "2022-11-28"
};
if (cached?.etag) headers["If-None-Match"] = cached.etag;
const init = {
method: "GET",
headers,
signal: requestSignal,
redirect: "error"
};
let response;
try {
requestSignal.throwIfAborted();
if (options.fetch) response = await options.fetch(`${API_ORIGIN}${path}`, init);
else {
const [{ fetchWithSsrFGuard }, { responseWithRelease }, { fetchWithRuntimeDispatcher }] = await Promise.all([
import("../../plugin-sdk/ssrf-runtime.js"),
import("../../plugin-sdk/fetch-runtime.js"),
import("../../plugin-sdk/runtime-fetch.js")
]);
const guarded = await fetchWithSsrFGuard({
url: `${API_ORIGIN}${path}`,
init,
capture: false,
requireHttps: true,
policy: { hostnameAllowlist: ["api.github.com"] },
maxRedirects: 0,
fetchImpl: fetchWithRuntimeDispatcher
});
response = responseWithRelease(guarded.response, guarded.release);
}
} catch {
throw new XGitHubError("GitHub request failed or was cancelled; the last verified set is unchanged.");
}
const exhausted = response.headers.get("x-ratelimit-remaining") === "0";
if (exhausted) retryAt = retryDeadline(response) ?? Date.now() + 6e4;
if (response.status === 304) {
await response.body?.cancel();
return cached ?? invalidResponse();
}
let body;
try {
const { readResponseWithLimit } = await import("../../plugin-sdk/response-limit-runtime.js");
const bytes = await readResponseWithLimit(response, 2e6, {
signal: requestSignal,
timeoutMs: 3e4
});
body = JSON.parse(bytes.toString());
} catch {
if (response.ok) invalidResponse();
}
if (!response.ok) {
const message = asOptionalRecord(body)?.message;
if (response.status === 429 || response.status === 403 && (exhausted || response.headers.has("retry-after") || typeof message === "string" && /secondary rate limit|abuse detection/i.test(message))) {
retryAt = retryDeadline(response) ?? Date.now() + 6e4;
rateLimited();
}
if ([
401,
403,
404
].includes(response.status) && path.startsWith("/repos/")) throw new XGitHubError(`GitHub cannot list repository collaborators (HTTP ${response.status}); check the repository and verifiedFromGitHub.token access and permissions.`);
throw new XGitHubError(`GitHub request failed (HTTP ${response.status}); check verifiedFromGitHub.token and retry.`);
}
return {
etag: response.headers.get("etag") ?? void 0,
value: parse(body)
};
}
return { async readCollaborators(repo, minPermission, signal) {
const [owner, repository, extra] = repo.split("/");
if (!owner || !LOGIN_PATTERN.test(owner) || !repository || !/^[a-z\d_.-]{1,100}$/i.test(repository) || [".", ".."].includes(repository) || extra !== void 0) throw new XGitHubError("verifiedFromGitHub.repo must be owner/name.");
const collaborators = /* @__PURE__ */ new Map();
for (let page = 1;; page++) {
const { value: rows } = await request(`/repos/${repo}/collaborators?affiliation=all&per_page=100&page=${page}`, (body) => {
if (!Array.isArray(body) || body.length > 100) invalidResponse();
return body.map((value) => {
const row = asOptionalRecord(value);
const permissions = asOptionalRecord(row?.permissions);
if (typeof row?.login !== "string" || !LOGIN_PATTERN.test(row.login) || typeof row.id !== "number" || !Number.isSafeInteger(row.id) || row.id <= 0 || !permissions || typeof permissions.push !== "boolean" || [permissions.maintain, permissions.admin].some((flag) => flag !== void 0 && typeof flag !== "boolean")) invalidResponse();
const permission = permissions.admin ? "admin" : permissions.maintain ? "maintain" : permissions.push ? "push" : void 0;
return {
githubLogin: row.login,
githubId: row.id,
permission
};
});
}, signal);
for (const row of rows) if (row.permission && PERMISSION_RANK[row.permission] >= PERMISSION_RANK[minPermission]) collaborators.set(row.githubLogin.toLowerCase(), {
githubLogin: row.githubLogin,
githubId: row.githubId,
permission: row.permission
});
if (rows.length < 100) break;
}
for (const login of profiles.keys()) if (!collaborators.has(login)) profiles.delete(login);
const result = [];
for (const [login, collaborator] of collaborators) {
const previous = profiles.get(login);
const cache = previous?.githubId === collaborator.githubId ? previous : { githubId: collaborator.githubId };
profiles.set(login, cache);
cache.profile = await request(`/users/${encodeURIComponent(login)}`, (body) => {
const row = asOptionalRecord(body);
if (typeof row?.login !== "string" || row.login.toLowerCase() !== login || row.id !== collaborator.githubId || row.twitter_username !== null && row.twitter_username !== void 0 && typeof row.twitter_username !== "string") invalidResponse();
const handle = xHandle(row.twitter_username);
return handle ? [handle] : [];
}, signal, cache.profile);
cache.social = await request(`/users/${encodeURIComponent(login)}/social_accounts`, (body) => {
if (!Array.isArray(body)) invalidResponse();
return body.flatMap((value) => {
const row = asOptionalRecord(value);
if (!row || typeof row.provider !== "string" || typeof row.url !== "string") invalidResponse();
const handle = row.provider.toLowerCase() === "twitter" || /^https?:\/\/(?:www\.)?(?:x|twitter)\.com\//i.test(row.url) ? xHandle(row.url) : void 0;
return handle ? [handle] : [];
});
}, signal, cache.social);
result.push({
githubLogin: collaborator.githubLogin,
permission: collaborator.permission,
xHandles: [.../* @__PURE__ */ new Set([...cache.profile.value, ...cache.social.value])]
});
}
return result;
} };
}
//#endregion
//#region extensions/x/src/verified-github-service.ts
function createXGitHubService(api) {
let activeAccounts = [];
return {
id: "x-verified-github",
apiVersion: 2,
reload: { configPrefixes: ["channels.x"] },
start(context) {
const readConfig = createRuntimeConfigReader(context.config);
const cfg = readConfig();
const allowlist = openXAllowlist(api.runtime);
activeAccounts = listXRuntimeAccountIds(cfg);
for (const accountId of activeAccounts) {
allowlist.invalidate(accountId);
const account = resolveXAccount(cfg, accountId);
const settings = account.config.verifiedFromGitHub;
if (!account.enabled || !account.configured || !settings?.repo) continue;
const capturedConfig = JSON.stringify(account.config);
const assertCurrent = () => {
context.scheduler.signal.throwIfAborted();
if (JSON.stringify(resolveXAccount(readConfig(), accountId).config) !== capturedConfig) throw new Error("X GitHub verification configuration changed");
};
let github;
let failed = false;
context.scheduler.schedule({
id: accountId,
delayMs: 0,
everyMs: (settings.refreshMinutes ?? 60) * 6e4,
run: async () => {
try {
assertCurrent();
const token = resolveSecretInputString({
value: settings.token,
defaults: cfg.secrets?.defaults,
path: `channels.x.accounts.${accountId}.verifiedFromGitHub.token`,
mode: "strict"
}).value;
if (!token) throw new XGitHubError("Configure verifiedFromGitHub.token with a GitHub credential that can list repository collaborators.");
github ??= createXGitHubReader({ token });
await syncXGitHub({
runtime: api.runtime,
account,
github,
getApi: () => getXApi(accountId, readConfig()),
signal: context.scheduler.signal,
assertCurrent
});
failed = false;
} catch (error) {
if (context.scheduler.signal.aborted) return;
assertCurrent();
const message = error instanceof XGitHubError || error instanceof XBudgetExceededError ? error.message : "GitHub verification failed; check the configured credentials and Gateway logs. If X token refresh failed, reauthorize the X account. The last verified set is unchanged.";
if (!failed) {
context.logger.warn(`X GitHub verification (${accountId}): ${message}`);
failed = true;
}
const previous = await allowlist.readGitHub(accountId, settings);
await allowlist.replaceGitHub(accountId, {
...previous ?? emptyXGitHubSnapshot(account),
stale: true,
message
}, assertCurrent);
}
}
});
}
},
stop() {
const allowlist = openXAllowlist(api.runtime);
for (const accountId of activeAccounts) allowlist.invalidate(accountId);
activeAccounts = [];
}
};
}
//#endregion
export { createXGitHubService };