Expose plugin registry API

This commit is contained in:
2026-05-08 16:37:20 -04:00
parent b7e1cd33e2
commit e6eda3bf1f
4 changed files with 891 additions and 1 deletions
+106
View File
@@ -1,4 +1,13 @@
import type { Env } from "./bindings.js";
import {
PluginRegistrySchemaError,
parsePluginRegistryDocument,
pluginCatalogDocument,
pluginDetailsDocument,
pluginPackageDocument,
pluginRegistryId,
pluginRegistryKvKey,
} from "./plugin_registry.js";
import {
ReleaseManifestSchemaError,
ReleaseSignatureQueryError,
@@ -24,6 +33,10 @@ export async function handleRequest(request: Request, env: Env): Promise<Respons
if (url.pathname === "/api/plugins/signing-keys") {
return handlePublicSigningKeys(request, env);
}
const pluginRoute = parsePluginRoute(url.pathname);
if (pluginRoute !== null) {
return handlePluginRoute(request, env, pluginRoute);
}
if (url.pathname === "/api/releases/manifest") {
return handleReleaseManifest(request, env);
}
@@ -34,6 +47,11 @@ export async function handleRequest(request: Request, env: Env): Promise<Respons
return jsonResponse({ error: "not_found" }, 404);
}
type PluginRoute =
| { kind: "catalog" }
| { kind: "details"; pluginId: string }
| { kind: "package"; pluginId: string };
async function handlePublicSigningKeys(request: Request, env: Env): Promise<Response> {
if (request.method !== "GET") {
return jsonResponse({ error: "method_not_allowed" }, 405, { Allow: "GET" });
@@ -58,6 +76,48 @@ async function handlePublicSigningKeys(request: Request, env: Env): Promise<Resp
}
}
async function handlePluginRoute(
request: Request,
env: Env,
route: PluginRoute,
): Promise<Response> {
if (request.method !== "GET") {
return jsonResponse({ error: "method_not_allowed" }, 405, { Allow: "GET" });
}
const kvKey = pluginRegistryKvKey(env.ELY_ENVIRONMENT);
const value = await env.ELY_KV.get(kvKey);
if (value === null) {
return jsonResponse({ error: "plugin_registry_unavailable" }, 503);
}
try {
const registry = parsePluginRegistryDocument(value);
if (route.kind === "catalog") {
return jsonResponse(pluginCatalogDocument(registry), 200, publicPluginCacheHeaders());
}
if (route.kind === "details") {
const document = pluginDetailsDocument(registry, route.pluginId);
if (document === null) {
return jsonResponse({ error: "plugin_not_found" }, 404);
}
return jsonResponse(document, 200, publicPluginCacheHeaders());
}
const document = pluginPackageDocument(registry, route.pluginId);
if (document === null) {
return jsonResponse({ error: "plugin_not_found" }, 404);
}
return jsonResponse(document, 200, publicPluginCacheHeaders());
} catch (error) {
if (error instanceof PluginRegistrySchemaError) {
return jsonResponse({ error: "plugin_registry_invalid" }, 500);
}
throw error;
}
}
async function handleReleaseManifest(request: Request, env: Env): Promise<Response> {
if (request.method !== "GET") {
return jsonResponse({ error: "method_not_allowed" }, 405, { Allow: "GET" });
@@ -124,6 +184,52 @@ async function handleReleaseSignature(
}
}
function parsePluginRoute(pathname: string): PluginRoute | null {
if (pathname === "/api/plugins") {
return { kind: "catalog" };
}
if (!pathname.startsWith("/api/plugins/")) {
return null;
}
const segments = pathname.split("/");
if (segments.length === 4) {
const pluginId = pluginRouteId(segments[3]);
if (pluginId === null) {
return null;
}
return { kind: "details", pluginId };
}
if (segments.length === 5 && segments[4] === "package") {
const pluginId = pluginRouteId(segments[3]);
if (pluginId === null) {
return null;
}
return { kind: "package", pluginId };
}
return null;
}
function pluginRouteId(segment: string | undefined): string | null {
if (segment === undefined) {
return null;
}
try {
return pluginRegistryId(segment);
} catch (error) {
if (error instanceof PluginRegistrySchemaError) {
return null;
}
throw error;
}
}
function publicPluginCacheHeaders(): Record<string, string> {
return { "Cache-Control": "public, max-age=300, stale-while-revalidate=60" };
}
function jsonResponse(
body: unknown,
status: number,