copilot_work_3 / src /lib /github.ts
arshvinw5's picture
add rate limit to submittions
416bdd1
Raw History Blame
2.52 kB
const REPO_PATH = /^\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?\/?$/;
export type GithubRepoRef = {
owner: string;
repo: string;
/** Lowercase `owner/repo`, used to detect duplicate submissions. */
repoKey: string;
/** Canonical URL stored in the database. */
canonicalUrl: string;
};
/** Returns undefined unless the value is a plain https://github.com/owner/repo URL. */
export function parseGithubRepoUrl(value: string): GithubRepoRef | undefined {
try {
const url = new URL(value);
if (
url.protocol !== "https:" ||
url.hostname !== "github.com" ||
url.username ||
url.password ||
url.search ||
url.hash
) {
return undefined;
}
const match = REPO_PATH.exec(url.pathname);
if (!match) return undefined;
const [, owner, repo] = match;
if (repo === "." || repo === ".." || owner === "." || owner === "..") return undefined;
return {
owner,
repo,
repoKey: `${owner}/${repo}`.toLowerCase(),
canonicalUrl: `https://github.com/${owner}/${repo}`,
};
} catch {
return undefined;
}
}
export type RepoVerification =
| { status: "ok" }
| { status: "not_found" }
| { status: "unavailable" };
/**
* Confirms that a public repository exists. `unavailable` means GitHub could not
* answer (rate limit, timeout, 5xx), so the caller should not reject the submission.
*/
export async function verifyRepo(
ref: Pick<GithubRepoRef, "owner" | "repo">,
fetchImpl: typeof fetch = fetch,
): Promise<RepoVerification> {
const headers: Record<string, string> = {
Accept: "application/vnd.github+json",
"User-Agent": "chainkit-directory",
"X-GitHub-Api-Version": "2022-11-28",
};
if (process.env.GITHUB_TOKEN) headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
try {
const response = await fetchImpl(
`https://api.github.com/repos/${encodeURIComponent(ref.owner)}/${encodeURIComponent(ref.repo)}`,
{ headers, signal: AbortSignal.timeout(5000), cache: "no-store" },
);
// Private repos return 404 to unauthenticated callers, which is what we want.
if (response.status === 404 || response.status === 451) return { status: "not_found" };
if (!response.ok) return { status: "unavailable" };
const data = (await response.json()) as { private?: boolean; disabled?: boolean };
if (data.private || data.disabled) return { status: "not_found" };
return { status: "ok" };
} catch {
return { status: "unavailable" };
}
}