@suluk/platform 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/catalog.ts +3 -0
- package/src/manifest.ts +6 -0
- package/src/plan.ts +9 -4
- package/test/plan.test.ts +21 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suluk/platform",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "The platform generator (C051): write one `definePlatform` manifest → it plans the shadcn-registry adds, generates the wired Hono entry, and merges each module's provision fragment into a single provision.config. The manifest compiles to a shadcn-add list + a C047 provision.config; the generator runs the adds + `@suluk/provision`. Turns the Suluk backend registry into a one-command platform. CANDIDATE tooling.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
package/src/catalog.ts
CHANGED
|
@@ -24,6 +24,8 @@ export const CATALOG: Record<string, CatalogEntry> = {
|
|
|
24
24
|
// the contract is a MIDDLEWARE mount: it installs the scope gate (enforceApiKeyScope) + GET /api/openapi.json. Place it
|
|
25
25
|
// after `auth` in the manifest so the gate runs after identity/apiKeyAuth set keyId/scopes. Derived + stateless.
|
|
26
26
|
contract: { mount: { kind: "middleware", symbol: "mountContract", from: "./routes/contract" } },
|
|
27
|
+
// the API-as-MCP server + OAuth discovery + connections — a middleware mount (registers /api/mcp + /.well-known/*).
|
|
28
|
+
mcp: { mount: { kind: "middleware", symbol: "mountMcp", from: "./routes/mcp" }, provision: { symbol: "mcpProvision", from: "./src/provision/mcp" } },
|
|
27
29
|
// feature routes mount under /api/* — where the caller-resolution + cors + rate-limit middleware live (toolfactory parity).
|
|
28
30
|
credits: { mount: { kind: "route", path: "/api/credits", symbol: "creditsRoutes", from: "./routes/credits" }, provision: { symbol: "creditsProvision", from: "./src/provision/credits" } },
|
|
29
31
|
keys: { mount: { kind: "route", path: "/api/keys", symbol: "keysRoutes", from: "./routes/keys" }, provision: { symbol: "keysProvision", from: "./src/provision/keys" } },
|
|
@@ -34,6 +36,7 @@ export const CATALOG: Record<string, CatalogEntry> = {
|
|
|
34
36
|
webhooks: { mount: { kind: "route", path: "/api/webhooks", symbol: "webhooksRoutes", from: "./routes/webhooks" }, provision: { symbol: "webhooksProvision", from: "./src/provision/webhooks" } },
|
|
35
37
|
// cross-cutting MIDDLEWARE (apply globally via app.use, emitted before any route) — not routed resources.
|
|
36
38
|
"rate-limit": { mount: { kind: "middleware", symbol: "mountRateLimit", from: "./services/rate-limit" } },
|
|
39
|
+
"rate-credit": { mount: { kind: "middleware", symbol: "mountRateCredit", from: "./services/rate-credit" } }, // credit-backed free-tier bucket (KV binding)
|
|
37
40
|
i18n: { mount: { kind: "middleware", symbol: "mountI18n", from: "./services/i18n" } },
|
|
38
41
|
reference: { mount: { kind: "route", path: "/api/reference", symbol: "referenceRoutes", from: "./routes/reference" } }, // derived doc render — no provision
|
|
39
42
|
admin: { mount: { kind: "route", path: "/api/admin", symbol: "adminRoutes", from: "./routes/admin" } }, // reads existing tables — no provision
|
package/src/manifest.ts
CHANGED
|
@@ -11,6 +11,12 @@ export interface PlatformManifest {
|
|
|
11
11
|
/** the services to include, in mount order — resolved against the catalog. `app` + `auth` are implied if any is listed
|
|
12
12
|
* but list them for clarity; the base + foundation always come first. */
|
|
13
13
|
services: string[];
|
|
14
|
+
/**
|
|
15
|
+
* Per-service static OPTIONS passed to that service's mount in the generated entry (a plain JSON-serializable object).
|
|
16
|
+
* E.g. enable MCP OAuth: `opts: { auth: { mcp: { loginPage, consentPage, resource, scopes } } }` → the entry emits
|
|
17
|
+
* `mountAuthRoutes(app, {...})`. Only JSON-safe values (no functions/env-refs — edit the generated entry for those).
|
|
18
|
+
*/
|
|
19
|
+
opts?: Record<string, Record<string, unknown>>;
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
/** Validate + return the manifest (throws on an empty service list). */
|
package/src/plan.ts
CHANGED
|
@@ -23,25 +23,30 @@ export function planPlatform(manifest: PlatformManifest): PlatformPlan {
|
|
|
23
23
|
return {
|
|
24
24
|
services,
|
|
25
25
|
adds: services.map((s) => `${manifest.registry}/${s}`),
|
|
26
|
-
entry: buildEntry(services),
|
|
26
|
+
entry: buildEntry(services, manifest.opts),
|
|
27
27
|
provisionConfig: buildProvisionConfig(services),
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
function buildEntry(services: string[]): string {
|
|
31
|
+
function buildEntry(services: string[], opts?: Record<string, Record<string, unknown>>): string {
|
|
32
32
|
const imports = ['import { createApp } from "./app";'];
|
|
33
33
|
const middleware: string[] = [];
|
|
34
34
|
const routes: string[] = [];
|
|
35
|
+
// per-service static options → a JSON literal passed to the mount (e.g. auth's mcp OAuth config). Empty ⇒ no 2nd arg.
|
|
36
|
+
const optOf = (s: string): string => {
|
|
37
|
+
const o = opts?.[s];
|
|
38
|
+
return o && Object.keys(o).length ? `, ${JSON.stringify(o)}` : "";
|
|
39
|
+
};
|
|
35
40
|
// TWO passes: ALL middleware mounts (app.use / handler) emit BEFORE any route mount, so a cross-cutting concern
|
|
36
41
|
// (auth, rate-limit, i18n) applies to every route regardless of where it sits in the manifest.
|
|
37
42
|
for (const s of services) {
|
|
38
43
|
const m = CATALOG[s].mount;
|
|
39
44
|
if (m.kind === "middleware") {
|
|
40
45
|
imports.push(`import { ${m.symbol} } from "${m.from}";`);
|
|
41
|
-
middleware.push(`${m.symbol}(app);`);
|
|
46
|
+
middleware.push(`${m.symbol}(app${optOf(s)});`);
|
|
42
47
|
} else if (m.kind === "route") {
|
|
43
48
|
imports.push(`import { ${m.symbol} } from "${m.from}";`);
|
|
44
|
-
routes.push(`app.route("${m.path}", ${m.symbol}());`);
|
|
49
|
+
routes.push(`app.route("${m.path}", ${m.symbol}(${optOf(s).replace(/^, /, "")}));`);
|
|
45
50
|
}
|
|
46
51
|
}
|
|
47
52
|
const body = ["const app = createApp();", ...middleware, ...routes];
|
package/test/plan.test.ts
CHANGED
|
@@ -35,6 +35,13 @@ describe("planPlatform — manifest → shadcn adds + entry + provision.config",
|
|
|
35
35
|
test("an unknown service throws", () => {
|
|
36
36
|
expect(() => planPlatform({ name: "x", registry: "r", services: ["nope"] })).toThrow(/unknown service/);
|
|
37
37
|
});
|
|
38
|
+
|
|
39
|
+
test("per-service opts are passed to the mount (e.g. auth mcp OAuth config)", () => {
|
|
40
|
+
const p = planPlatform(definePlatform({ name: "o", registry: "acme/reg", services: ["auth", "credits"], opts: { auth: { mcp: { resource: "https://api.x", scopes: ["credits:read"] } } } }));
|
|
41
|
+
expect(p.entry).toContain('mountAuthRoutes(app, {"mcp":{"resource":"https://api.x","scopes":["credits:read"]}});');
|
|
42
|
+
// a service with no opts still gets the bare call.
|
|
43
|
+
expect(p.entry).toContain('app.route("/api/credits", creditsRoutes());');
|
|
44
|
+
});
|
|
38
45
|
});
|
|
39
46
|
|
|
40
47
|
describe("cost (route + provision) + dev modules (journeys/audit — files only)", () => {
|
|
@@ -101,6 +108,20 @@ describe("cost (route + provision) + dev modules (journeys/audit — files only)
|
|
|
101
108
|
expect(p.provisionConfig).not.toContain("adminProvision");
|
|
102
109
|
});
|
|
103
110
|
|
|
111
|
+
test("mcp is a middleware mount (server + discovery + connections) with a provision fragment", () => {
|
|
112
|
+
const p = planPlatform(definePlatform({ name: "m", registry: "acme/reg", services: ["auth", "contract", "mcp", "credits"] }));
|
|
113
|
+
expect(p.entry).toContain('import { mountMcp } from "./routes/mcp";');
|
|
114
|
+
expect(p.entry).toContain("mountMcp(app);");
|
|
115
|
+
expect(p.entry).not.toContain('app.route("/api/mcp"'); // it's a mount, not a route
|
|
116
|
+
expect(p.provisionConfig).toContain("mcpProvision");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("rate-credit is a middleware mount (KV binding) with NO provision fragment", () => {
|
|
120
|
+
const p = planPlatform(definePlatform({ name: "rc", registry: "acme/reg", services: ["auth", "rate-credit", "credits"] }));
|
|
121
|
+
expect(p.entry).toContain("mountRateCredit(app);");
|
|
122
|
+
expect(p.provisionConfig).not.toContain("rateCreditProvision");
|
|
123
|
+
});
|
|
124
|
+
|
|
104
125
|
test("dev modules add shadcn refs but NO entry mount and NO provision fragment", () => {
|
|
105
126
|
expect(plan.adds).toContain("acme/reg/journeys");
|
|
106
127
|
expect(plan.adds).toContain("acme/reg/audit");
|