feat: expose gateway plugin registry
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
"name": "cfw-gateway",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.33.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "wrangler dev",
|
||||
|
||||
@@ -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
174
src/plugin-registry.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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" };
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user