mirror of
https://github.com/herdrdev/herdr.git
synced 2026-09-21 16:01:04 +00:00
feat: add plugin marketplace blacklist
This commit is contained in:
@@ -9,6 +9,20 @@ class MemoryR2 {
|
||||
}
|
||||
}
|
||||
|
||||
class MemoryKV {
|
||||
constructor(private readonly keyNames: string[]) {}
|
||||
|
||||
async list(options?: { prefix?: string }): Promise<{
|
||||
keys: Array<{ name: string }>;
|
||||
}> {
|
||||
return {
|
||||
keys: this.keyNames
|
||||
.filter((name) => !options?.prefix || name.startsWith(options.prefix))
|
||||
.map((name) => ({ name })),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function repo(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
||||
return {
|
||||
id: 1,
|
||||
@@ -34,9 +48,10 @@ function repo(overrides: Record<string, unknown> = {}): Record<string, unknown>
|
||||
};
|
||||
}
|
||||
|
||||
function env(bucket = new MemoryR2()): Env {
|
||||
function env(bucket = new MemoryR2(), blacklist?: MemoryKV): Env {
|
||||
return {
|
||||
PLUGIN_MARKETPLACE_BUCKET: bucket,
|
||||
PLUGIN_MARKETPLACE_BLACKLIST: blacklist,
|
||||
GITHUB_TOKEN: "token",
|
||||
};
|
||||
}
|
||||
@@ -191,6 +206,68 @@ describe("refreshPlugins", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("excludes repositories listed in the KV blacklist", async () => {
|
||||
const fetch = async (): Promise<Response> =>
|
||||
Response.json({
|
||||
total_count: 2,
|
||||
items: [
|
||||
repo({
|
||||
id: 1,
|
||||
full_name: "example/not-a-plugin",
|
||||
owner: { login: "example" },
|
||||
name: "not-a-plugin",
|
||||
html_url: "https://github.com/example/not-a-plugin",
|
||||
}),
|
||||
repo({
|
||||
id: 2,
|
||||
full_name: "ogulcancelik/herdr-plugin-example",
|
||||
owner: { login: "ogulcancelik" },
|
||||
name: "herdr-plugin-example",
|
||||
html_url: "https://github.com/ogulcancelik/herdr-plugin-example",
|
||||
}),
|
||||
],
|
||||
});
|
||||
const bucket = new MemoryR2();
|
||||
|
||||
const result = await refreshPlugins(env(bucket, new MemoryKV(["repo:example/not-a-plugin"])), {
|
||||
fetch,
|
||||
logger: { error() {} },
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
const snapshot = JSON.parse(bucket.objects.get("plugins/index.json")?.value ?? "");
|
||||
expect(snapshot.plugins.map((plugin: { fullName: string }) => plugin.fullName)).toEqual([
|
||||
"ogulcancelik/herdr-plugin-example",
|
||||
]);
|
||||
});
|
||||
|
||||
test("writes an empty snapshot when every listable repository is blacklisted", async () => {
|
||||
const fetch = async (): Promise<Response> =>
|
||||
Response.json({
|
||||
total_count: 1,
|
||||
items: [
|
||||
repo({
|
||||
id: 1,
|
||||
full_name: "example/not-a-plugin",
|
||||
owner: { login: "example" },
|
||||
name: "not-a-plugin",
|
||||
html_url: "https://github.com/example/not-a-plugin",
|
||||
}),
|
||||
],
|
||||
});
|
||||
const bucket = new MemoryR2();
|
||||
await bucket.put("plugins/index.json", '{"schemaVersion":1,"plugins":[{"id":1}]}');
|
||||
|
||||
const result = await refreshPlugins(env(bucket, new MemoryKV(["repo:example/not-a-plugin"])), {
|
||||
fetch,
|
||||
logger: { error() {} },
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
const snapshot = JSON.parse(bucket.objects.get("plugins/index.json")?.value ?? "");
|
||||
expect(snapshot.plugins).toEqual([]);
|
||||
});
|
||||
|
||||
test("marks snapshots truncated at the GitHub search cap", async () => {
|
||||
const fetch = async (): Promise<Response> => {
|
||||
const items = Array.from({ length: 100 }, (_, index) =>
|
||||
|
||||
@@ -3,6 +3,7 @@ const SNAPSHOT_CACHE_CONTROL = "public, max-age=300, s-maxage=1800, stale-while-
|
||||
const GITHUB_QUERY = "topic:herdr-plugin is:public";
|
||||
const GITHUB_API_VERSION = "2022-11-28";
|
||||
const GITHUB_SEARCH_URL = "https://api.github.com/search/repositories";
|
||||
const BLACKLIST_REPO_KEY_PREFIX = "repo:";
|
||||
const PER_PAGE = 100;
|
||||
const MAX_REPOS = 1000;
|
||||
const REQUEST_TIMEOUT_MS = 10_000;
|
||||
@@ -20,6 +21,13 @@ type R2Bucket = {
|
||||
): Promise<unknown>;
|
||||
};
|
||||
|
||||
type KVNamespace = {
|
||||
list(options?: { prefix?: string; cursor?: string }): Promise<{
|
||||
keys: Array<{ name: string }>;
|
||||
cursor?: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
type ExecutionContext = {
|
||||
waitUntil(promise: Promise<unknown>): void;
|
||||
};
|
||||
@@ -28,6 +36,7 @@ type ScheduledController = unknown;
|
||||
|
||||
export type Env = {
|
||||
PLUGIN_MARKETPLACE_BUCKET: R2Bucket;
|
||||
PLUGIN_MARKETPLACE_BLACKLIST?: KVNamespace;
|
||||
GITHUB_TOKEN?: string;
|
||||
};
|
||||
|
||||
@@ -99,11 +108,19 @@ export async function refreshPlugins(
|
||||
|
||||
const fetchFn = options.fetch ?? fetch;
|
||||
const result = await fetchGitHubRepositories(fetchFn, token);
|
||||
const plugins = normalizeRepositories(result.repositories);
|
||||
if (plugins.length === 0) {
|
||||
const normalizedPlugins = normalizeRepositories(result.repositories);
|
||||
if (normalizedPlugins.length === 0) {
|
||||
throw new Error("GitHub returned no listable plugin repositories");
|
||||
}
|
||||
|
||||
const blockedRepositories = await readBlacklistedRepositories(env);
|
||||
const plugins =
|
||||
blockedRepositories.size === 0
|
||||
? normalizedPlugins
|
||||
: normalizedPlugins.filter(
|
||||
(plugin) => !blockedRepositories.has(plugin.fullName.toLowerCase()),
|
||||
);
|
||||
|
||||
const snapshot: PluginSnapshot = {
|
||||
schemaVersion: 1,
|
||||
generatedAt: (options.now ?? new Date()).toISOString(),
|
||||
@@ -216,6 +233,28 @@ export function normalizeRepositories(repositories: GitHubRepository[]): PluginL
|
||||
.sort(comparePlugins);
|
||||
}
|
||||
|
||||
async function readBlacklistedRepositories(env: Env): Promise<Set<string>> {
|
||||
const kv = env.PLUGIN_MARKETPLACE_BLACKLIST;
|
||||
const blockedRepositories = new Set<string>();
|
||||
if (!kv) {
|
||||
return blockedRepositories;
|
||||
}
|
||||
|
||||
let cursor: string | undefined;
|
||||
do {
|
||||
const page = await kv.list({ prefix: BLACKLIST_REPO_KEY_PREFIX, cursor });
|
||||
for (const key of page.keys) {
|
||||
const repository = key.name.slice(BLACKLIST_REPO_KEY_PREFIX.length).trim().toLowerCase();
|
||||
if (repository.includes("/")) {
|
||||
blockedRepositories.add(repository);
|
||||
}
|
||||
}
|
||||
cursor = page.cursor;
|
||||
} while (cursor);
|
||||
|
||||
return blockedRepositories;
|
||||
}
|
||||
|
||||
function normalizeRepository(repo: GitHubRepository): PluginListing | null {
|
||||
if (
|
||||
readBoolean(repo.disabled) ||
|
||||
|
||||
@@ -7,5 +7,9 @@ workers_dev = false
|
||||
binding = "PLUGIN_MARKETPLACE_BUCKET"
|
||||
bucket_name = "herdr-plugin-marketplace"
|
||||
|
||||
[[kv_namespaces]]
|
||||
binding = "PLUGIN_MARKETPLACE_BLACKLIST"
|
||||
id = "6504b3b84171492db56e805f6aad1686"
|
||||
|
||||
[triggers]
|
||||
crons = ["*/30 * * * *"]
|
||||
|
||||
Reference in New Issue
Block a user