@suluk/platform 0.6.0 → 0.7.0

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suluk/platform",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
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/generate.ts CHANGED
@@ -85,6 +85,12 @@ export async function generatePlatform(input: PlatformManifest | Platform, opts:
85
85
  log("▸ writing provision.config.ts");
86
86
  await opts.write("provision.config.ts", plan.provisionConfig);
87
87
  written.push("src/index.ts", "provision.config.ts");
88
+ // the composed contract surface (one fragment per module) — only when the `contract` service is installed.
89
+ if (plan.contractOps) {
90
+ log("▸ writing src/contract.ops.ts");
91
+ await opts.write("src/contract.ops.ts", plan.contractOps);
92
+ written.push("src/contract.ops.ts");
93
+ }
88
94
  // the bun MOCK-PROVIDER dev server + the state-purge helper — only when the manifest sets `local: true`.
89
95
  if (plan.devEntry) {
90
96
  log("▸ writing src/dev.ts");
package/src/plan.ts CHANGED
@@ -16,6 +16,9 @@ export interface PlatformPlan {
16
16
  entry: string;
17
17
  /** the generated `provision.config.ts` content. */
18
18
  provisionConfig: string;
19
+ /** the generated `src/contract.ops.ts` — the COMPOSED contract surface (one `RouteContract[]` fragment per module). Present
20
+ * ONLY when the `contract` service is installed; the base `src/contract.ts` consumes its `ALL_OPS`. */
21
+ contractOps?: string;
19
22
  /** the generated `package.json` content (the FRAMEWORK baseline — `generate` merges it with any existing so app-added
20
23
  * deps/scripts survive). @suluk/* on "latest" so fixes flow via `bun update`; ecosystem deps on pinned ranges. */
21
24
  packageJson: string;
@@ -88,6 +91,7 @@ export function planPlatform(input: PlatformManifest | Platform): PlatformPlan {
88
91
  adds: services.map((s) => `${catalog[s].registry ?? manifest.registry}/${s}`),
89
92
  entry: buildEntry(services, manifest.opts, wiring, catalog, local),
90
93
  provisionConfig: buildProvisionConfig(services, catalog),
94
+ ...(services.includes("contract") ? { contractOps: buildContractOps(services, catalog) } : {}),
91
95
  packageJson: buildPackageJson(manifest.name, services, catalog, local),
92
96
  tsconfig: buildTsconfig(local),
93
97
  componentsJson: buildComponentsJson(),
@@ -846,6 +850,25 @@ if (token && account && d1Id) {
846
850
  `;
847
851
  }
848
852
 
853
+ /**
854
+ * `src/contract.ops.ts` — the COMPOSED contract surface: one `RouteContract[]` fragment per installed module (each module
855
+ * OWNS its ops next to its routes), spread into `ALL_OPS`. Mirrors {@link buildProvisionConfig}. The base `src/contract.ts`
856
+ * consumes `ALL_OPS` (adding the system op + the derivations), so adding/changing a module's routes only touches THAT
857
+ * module's fragment — the central contract can never drift from the routes again.
858
+ */
859
+ function buildContractOps(services: string[], catalog: Record<string, Service> = CORE_SERVICES): string {
860
+ const frags = services.map((s) => catalog[s].contract).filter((c): c is NonNullable<typeof c> => !!c);
861
+ const imports = frags.map((f) => `import { ${f.symbol} } from "${f.from}";`);
862
+ return [
863
+ "// AUTO-GENERATED by @suluk/platform — the composed contract surface (one fragment per installed module).",
864
+ 'import type { DocumentedRoute } from "@suluk/hono";',
865
+ ...imports,
866
+ "",
867
+ `export const ALL_OPS: readonly DocumentedRoute[] = [${frags.map((f) => `...${f.symbol}`).join(", ")}];`,
868
+ "",
869
+ ].join("\n");
870
+ }
871
+
849
872
  function buildProvisionConfig(services: string[], catalog: Record<string, Service> = CORE_SERVICES): string {
850
873
  const frags = services.map((s) => catalog[s].provision).filter((p): p is NonNullable<typeof p> => !!p);
851
874
  const imports = frags.map((f) => `import { ${f.symbol} } from "${f.from}";`);
package/src/service.ts CHANGED
@@ -50,6 +50,8 @@ export interface EnvVar {
50
50
  export interface CatalogEntry {
51
51
  mount: Mount;
52
52
  provision?: { symbol: string; from: string };
53
+ /** the module's CONTRACT fragment — its `RouteContract[]` (ops), composed into `src/contract.ops.ts` (mirrors `provision`). */
54
+ contract?: { symbol: string; from: string };
53
55
  deps?: string[];
54
56
  env?: EnvVar[];
55
57
  }
@@ -109,6 +111,8 @@ export interface Service<SO = {}, BO = {}> {
109
111
  readonly registry?: string; // owning registry (multi-registry, Phase 4); default = the manifest's core alias
110
112
  readonly mount: Mount;
111
113
  readonly provision?: { symbol: string; from: string };
114
+ /** the module's CONTRACT fragment — its `RouteContract[]` (ops), composed into `src/contract.ops.ts` (mirrors `provision`). */
115
+ readonly contract?: { symbol: string; from: string };
112
116
  readonly deps?: string[];
113
117
  readonly env?: EnvVar[];
114
118
  readonly serviceOpts?: Schema<SO>; // how THIS service works → the ENTRY (mount 2nd arg) [Phase 2]
@@ -163,7 +167,7 @@ export interface CoreServiceOptsMap {
163
167
  /** Project a Service onto the legacy {@link CatalogEntry} shape the C051 generator reads. Field-for-field — so a derived
164
168
  * CATALOG is behaviourally identical to the old hardcoded one (proven by the Phase-0 golden lock). */
165
169
  export function toCatalogEntry(s: Service): CatalogEntry {
166
- return { mount: s.mount, provision: s.provision, deps: s.deps, env: s.env };
170
+ return { mount: s.mount, provision: s.provision, contract: s.contract, deps: s.deps, env: s.env };
167
171
  }
168
172
 
169
173
  /**
@@ -214,12 +218,13 @@ export const authService = defineService({
214
218
  });
215
219
 
216
220
  export const contractService = defineService({ id: "contract", mount: { kind: "middleware", symbol: "mountContract", from: "./routes/contract" }, deps: ["@suluk/hono", "zod"] });
217
- export const mcpService = defineService({ id: "mcp", mount: { kind: "middleware", symbol: "mountMcp", from: "./routes/mcp" }, provision: { symbol: "mcpProvision", from: "./src/provision/mcp" }, deps: ["@suluk/mcp", "better-auth"] });
221
+ export const mcpService = defineService({ id: "mcp", mount: { kind: "middleware", symbol: "mountMcp", from: "./routes/mcp" }, provision: { symbol: "mcpProvision", from: "./src/provision/mcp" }, contract: { symbol: "mcpOps", from: "./contract/mcp" }, deps: ["@suluk/mcp", "better-auth"] });
218
222
 
219
223
  export const creditsService = defineService({
220
224
  id: "credits",
221
225
  mount: { kind: "route", path: "/api/credits", symbol: "creditsRoutes", from: "./routes/credits" },
222
226
  provision: { symbol: "creditsProvision", from: "./src/provision/credits" },
227
+ contract: { symbol: "creditsOps", from: "./contract/credits" },
223
228
  deps: ["@suluk/credits"],
224
229
  compose: {
225
230
  offers: {
@@ -246,12 +251,13 @@ export const creditsService = defineService({
246
251
  },
247
252
  });
248
253
 
249
- export const keysService = defineService({ id: "keys", mount: { kind: "route", path: "/api/keys", symbol: "keysRoutes", from: "./routes/keys" }, provision: { symbol: "keysProvision", from: "./src/provision/keys" }, deps: ["@suluk/keys"] });
254
+ export const keysService = defineService({ id: "keys", mount: { kind: "route", path: "/api/keys", symbol: "keysRoutes", from: "./routes/keys" }, provision: { symbol: "keysProvision", from: "./src/provision/keys" }, contract: { symbol: "keysOps", from: "./contract/keys" }, deps: ["@suluk/keys"] });
250
255
 
251
256
  export const billingService = defineService({
252
257
  id: "billing",
253
258
  mount: { kind: "route", path: "/api/billing", symbol: "billingRoutes", from: "./routes/billing" },
254
259
  provision: { symbol: "billingProvision", from: "./src/provision/billing" },
260
+ contract: { symbol: "billingOps", from: "./contract/billing" },
255
261
  deps: ["@suluk/billing", "@suluk/payments", "@suluk/credits"],
256
262
  env: [
257
263
  { name: "STRIPE_SECRET_KEY", required: true, secret: true, hint: "your Stripe secret key" },
@@ -259,12 +265,13 @@ export const billingService = defineService({
259
265
  ],
260
266
  });
261
267
 
262
- export const costService = defineService({ id: "cost", mount: { kind: "route", path: "/api/cost", symbol: "costRoutes", from: "./routes/cost" }, provision: { symbol: "costProvision", from: "./src/provision/cost" }, deps: ["@suluk/cost"] });
263
- export const erasureService = defineService({ id: "erasure", mount: { kind: "route", path: "/api/erasure", symbol: "erasureRoutes", from: "./routes/erasure" }, provision: { symbol: "erasureProvision", from: "./src/provision/erasure" }, deps: ["@suluk/better-auth"] });
268
+ export const costService = defineService({ id: "cost", mount: { kind: "route", path: "/api/cost", symbol: "costRoutes", from: "./routes/cost" }, provision: { symbol: "costProvision", from: "./src/provision/cost" }, contract: { symbol: "costOps", from: "./contract/cost" }, deps: ["@suluk/cost"] });
269
+ export const erasureService = defineService({ id: "erasure", mount: { kind: "route", path: "/api/erasure", symbol: "erasureRoutes", from: "./routes/erasure" }, provision: { symbol: "erasureProvision", from: "./src/provision/erasure" }, contract: { symbol: "erasureOps", from: "./contract/erasure" }, deps: ["@suluk/better-auth"] });
264
270
 
265
271
  export const emailService = defineService({
266
272
  id: "email",
267
- mount: { kind: "route", path: "/api/email", symbol: "emailRoutes", from: "./routes/email" }, // stateless binding — no provision fragment (C052)
273
+ mount: { kind: "route", path: "/api/email", symbol: "emailRoutes", from: "./routes/email" }, // stateless binding — no provision fragment (C052) // stateless binding — no provision fragment (C052)
274
+ contract: { symbol: "emailOps", from: "./contract/email" },
268
275
  deps: ["@suluk/email"],
269
276
  env: [
270
277
  { name: "RESEND_API_KEY", secret: true, hint: "omit → the console provider (dev)" },
@@ -279,6 +286,7 @@ export const webhooksService = defineService({
279
286
  id: "webhooks",
280
287
  mount: { kind: "route", path: "/api/webhooks", symbol: "webhooksRoutes", from: "./routes/webhooks" },
281
288
  provision: { symbol: "webhooksProvision", from: "./src/provision/webhooks" },
289
+ contract: { symbol: "webhooksOps", from: "./contract/webhooks" },
282
290
  deps: ["@suluk/payments"],
283
291
  env: [{ name: "STRIPE_WEBHOOK_SECRET", required: true, secret: true, hint: "verifies inbound Stripe events (POST /api/webhooks/stripe)" }],
284
292
  });
@@ -286,9 +294,9 @@ export const webhooksService = defineService({
286
294
  export const rateLimitService = defineService({ id: "rate-limit", mount: { kind: "middleware", symbol: "mountRateLimit", from: "./services/rate-limit" }, deps: ["@suluk/hono"] });
287
295
  export const rateCreditService = defineService({ id: "rate-credit", mount: { kind: "middleware", symbol: "mountRateCredit", from: "./services/rate-credit" } }); // credit-backed free-tier bucket (KV binding)
288
296
  export const i18nService = defineService({ id: "i18n", mount: { kind: "middleware", symbol: "mountI18n", from: "./services/i18n" }, deps: ["@suluk/i18n"] });
289
- export const referenceService = defineService({ id: "reference", mount: { kind: "route", path: "/api/reference", symbol: "referenceRoutes", from: "./routes/reference" }, deps: ["@suluk/reference"] }); // derived — no provision
290
- export const adminService = defineService({ id: "admin", mount: { kind: "route", path: "/api/admin", symbol: "adminRoutes", from: "./routes/admin" }, deps: ["@suluk/credits"], env: [{ name: "SUPERADMIN_EMAILS", secret: true, hint: "comma/space-separated admin emails → the admin scope (secret-surfaced so they stay out of git plaintext)" }] }); // reads existing tables — no provision
291
- export const logsService = defineService({ id: "logs", mount: { kind: "route", path: "/api/logs", symbol: "logsRoutes", from: "./routes/logs" }, provision: { symbol: "logsProvision", from: "./src/provision/logs" } });
297
+ export const referenceService = defineService({ id: "reference", mount: { kind: "route", path: "/api/reference", symbol: "referenceRoutes", from: "./routes/reference" }, contract: { symbol: "referenceOps", from: "./contract/reference" }, deps: ["@suluk/reference"] }); // derived — no provision
298
+ export const adminService = defineService({ id: "admin", mount: { kind: "route", path: "/api/admin", symbol: "adminRoutes", from: "./routes/admin" }, contract: { symbol: "adminOps", from: "./contract/admin" }, deps: ["@suluk/credits"], env: [{ name: "SUPERADMIN_EMAILS", secret: true, hint: "comma/space-separated admin emails → the admin scope (secret-surfaced so they stay out of git plaintext)" }] }); // reads existing tables — no provision
299
+ export const logsService = defineService({ id: "logs", mount: { kind: "route", path: "/api/logs", symbol: "logsRoutes", from: "./routes/logs" }, provision: { symbol: "logsProvision", from: "./src/provision/logs" }, contract: { symbol: "logsOps", from: "./contract/logs" } });
292
300
  export const journeysService = defineService({ id: "journeys", mount: { kind: "dev" }, deps: ["@suluk/journeys"] });
293
301
  export const auditService = defineService({ id: "audit", mount: { kind: "dev" }, deps: ["@suluk/cockpit", "@suluk/harden"] });
294
302