feat: expose gateway plugin registry

This commit is contained in:
2026-06-11 19:46:13 -07:00
parent 21f43660c6
commit e14ed1ec2c
6 changed files with 232 additions and 0 deletions

View File

@@ -2,6 +2,7 @@
"name": "cfw-gateway",
"version": "0.1.0",
"private": true,
"packageManager": "pnpm@10.33.0",
"type": "module",
"scripts": {
"dev": "wrangler dev",

View File

@@ -2,6 +2,7 @@ import { Scalar } from "@scalar/hono-api-reference";
import { Hono } from "hono";
import type { Env } from "./env";
import { composeOpenApiDocument, fetchServiceOpenApiDocument } from "./openapi";
import { createPluginRegistryDocument } from "./plugin-registry";
import { resolveRoute } from "./route-policy";
const app = new Hono<{ Bindings: Env }>();
@@ -74,6 +75,12 @@ app.all("*", async (c) => {
}
}
if (target.kind === "gateway-plugin-manifest") {
return c.json(createPluginRegistryDocument(), 200, {
"cache-control": "public, max-age=60, stale-while-revalidate=300",
});
}
if (target.kind === "gateway-docs") {
return gatewayReference(c, async () => undefined);
}

174
src/plugin-registry.ts Normal file
View File

@@ -0,0 +1,174 @@
export type PluginStatus = "enabled" | "planned" | "disabled";
export type PluginCapability =
| "routes"
| "menu"
| "settings"
| "ops"
| "notifications"
| "user-center";
export type PluginSlotContribution = {
plugin: string;
slot:
| "sidebar.primary"
| "sidebar.secondary"
| "topbar.actions"
| "notifications.panel"
| "user-menu.items"
| "dashboard.cards"
| "settings.sections";
label: string;
path: string;
order: number;
permission?: string;
};
export type PluginNavigationContribution = {
label: string;
path: string;
order: number;
permission?: string;
};
export type GatewayPluginManifest = {
name: string;
version: string;
entry: string;
module: string;
owner: string;
status: PluginStatus;
routes: string[];
home: {
path: string;
title: string;
description?: string;
};
navigation: PluginNavigationContribution[];
slots: PluginSlotContribution[];
permissions: string[];
capabilities: PluginCapability[];
};
export type GatewayPluginRegistryDocument = {
schemaVersion: 1;
kind: "cfw-plugin-registry";
generatedAt: string;
plugins: GatewayPluginManifest[];
};
const plugins: GatewayPluginManifest[] = [
{
name: "account-center",
version: "0.1.0",
entry: "account_center@/apps/account-center/mf-manifest.json",
module: "./App",
owner: "cfw-auth",
status: "enabled",
routes: ["/account-center/*"],
home: {
path: "/account-center",
title: "Account Center",
description: "User, organization and API key management.",
},
navigation: [
{
label: "Account Center",
path: "/account-center",
order: 100,
permission: "session:read",
},
],
slots: [
{
plugin: "account-center",
slot: "user-menu.items",
label: "API keys",
path: "/account/api-keys",
order: 100,
permission: "api-key:manage",
},
],
permissions: ["session:read", "api-key:manage"],
capabilities: ["routes", "menu", "user-center"],
},
{
name: "ops-console",
version: "0.1.0",
entry: "ops_console@/apps/ops-console/mf-manifest.json",
module: "./App",
owner: "cfw-ops",
status: "planned",
routes: ["/ops-console/*"],
home: {
path: "/ops-console",
title: "Ops Console",
description: "Observations, incidents and notification delivery.",
},
navigation: [
{
label: "Ops Console",
path: "/ops-console",
order: 200,
permission: "ops:read",
},
],
slots: [
{
plugin: "ops-console",
slot: "notifications.panel",
label: "Incidents",
path: "/ops/incidents",
order: 100,
permission: "incident:read",
},
],
permissions: ["ops:read", "incident:write"],
capabilities: ["routes", "menu", "ops", "notifications"],
},
{
name: "billing",
version: "0.1.0",
entry: "billing@/apps/billing/mf-manifest.json",
module: "./App",
owner: "business-worker",
status: "planned",
routes: ["/billing/*"],
home: {
path: "/billing",
title: "Billing",
description: "Business-owned billing entry page.",
},
navigation: [
{
label: "Billing",
path: "/billing",
order: 300,
permission: "billing:read",
},
],
slots: [
{
plugin: "billing",
slot: "sidebar.primary",
label: "Billing reports",
path: "/billing/reports",
order: 300,
permission: "billing:read",
},
],
permissions: ["billing:read"],
capabilities: ["routes", "menu", "settings"],
},
];
export function createPluginRegistryDocument(
generatedAt: string = new Date().toISOString(),
): GatewayPluginRegistryDocument {
return {
schemaVersion: 1,
kind: "cfw-plugin-registry",
generatedAt,
plugins,
};
}

View File

@@ -7,6 +7,7 @@ export type RouteTarget =
| { kind: "gateway-docs" }
| { kind: "gateway-health" }
| { kind: "gateway-openapi" }
| { kind: "gateway-plugin-manifest" }
| { kind: "not-found" };
export function resolveRoute(request: Request, env: Env): RouteTarget {
@@ -24,6 +25,10 @@ export function resolveRoute(request: Request, env: Env): RouteTarget {
return { kind: "gateway-openapi" };
}
if (url.pathname === "/mf-manifest.json") {
return { kind: "gateway-plugin-manifest" };
}
if (url.pathname.startsWith("/api/auth/")) {
return { kind: "auth" };
}

View File

@@ -112,6 +112,48 @@ describe("cfw-gateway worker", () => {
expect(schema.paths["/api/scheduler/jobs"]).toBeTruthy();
});
it("serves the gateway-owned plugin manifest without consulting services", async () => {
const auth = createJsonService(401, { authenticated: false });
const ops = createJsonService(404, { service: "ops" });
const scheduler = createJsonService(404, { service: "scheduler" });
const env = createEnv({
AUTH: auth,
OPS: ops,
SCHEDULER: scheduler,
});
const response = await worker.fetch(new Request("http://gateway.local/mf-manifest.json"), env);
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toContain("application/json");
expect(response.headers.get("cache-control")).toBe("public, max-age=60, stale-while-revalidate=300");
const manifest = await response.json() as {
schemaVersion: number;
kind: string;
plugins: Array<{
name: string;
entry: string;
routes: string[];
status: string;
}>;
};
expect(manifest.schemaVersion).toBe(1);
expect(manifest.kind).toBe("cfw-plugin-registry");
expect(manifest.plugins).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: "account-center",
entry: "account_center@/apps/account-center/mf-manifest.json",
routes: ["/account-center/*"],
status: "enabled",
}),
]),
);
expect(auth.fetch).not.toHaveBeenCalled();
expect(ops.fetch).not.toHaveBeenCalled();
expect(scheduler.fetch).not.toHaveBeenCalled();
});
it("returns a diagnostic error when aggregated OpenAPI fetch fails", async () => {
const env = createEnv({
AUTH: {

View File

@@ -16,6 +16,9 @@ describe("route policy", () => {
expect(resolveRoute(new Request("http://gateway.local/openapi.json"), env)).toEqual({
kind: "gateway-openapi",
});
expect(resolveRoute(new Request("http://gateway.local/mf-manifest.json"), env)).toEqual({
kind: "gateway-plugin-manifest",
});
});
it("routes each service prefix to its service binding", () => {