@cosmicdrift/kumiko-dev-server 0.26.0 → 0.28.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": "@cosmicdrift/kumiko-dev-server",
3
- "version": "0.26.0",
3
+ "version": "0.28.0",
4
4
  "description": "Development server bootstrap for Kumiko apps. Bundles the client, mints dev-JWTs, injects the resolved AppSchema, and seeds an admin. Not for production.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -68,6 +68,19 @@ const widgetFeature = defineFeature("prod-probe", (r) => {
68
68
  access: { roles: ["anonymous"] },
69
69
  handler: async () => ({ pong: true }),
70
70
  });
71
+ // SystemAdmin-gated write — Ziel des extraRoutes.dispatchSystemWrite-
72
+ // Tests: Echo von user.tenantId + roles beweist, dass der Dispatch
73
+ // durch den echten Dispatcher (Zod + Access-Check) läuft und der
74
+ // auto-konstruierte SystemUser den Ziel-Tenant trägt.
75
+ r.writeHandler({
76
+ name: "probe-write",
77
+ schema: z.object({ note: z.string() }),
78
+ access: { roles: ["SystemAdmin"] },
79
+ handler: async (event) => ({
80
+ isSuccess: true as const,
81
+ data: { tenantSeen: event.user.tenantId, roles: event.user.roles },
82
+ }),
83
+ });
71
84
  });
72
85
 
73
86
  const TENANT_ID = "00000000-0000-4000-8000-000000000001";
@@ -206,6 +219,41 @@ describe("runProdApp", () => {
206
219
  expect(body).toContain('<probe ok="true" />');
207
220
  });
208
221
 
222
+ test("extraRoutes-deps: dispatchSystemWrite schreibt als SystemAdmin des Ziel-Tenants, registry verfügbar", async () => {
223
+ // Das ist das Wiring für Provider-Webhook-Routes (billing-foundation
224
+ // createSubscriptionWebhookHandler): die Route authentifiziert via
225
+ // Provider-Signatur und schreibt dann am JWT-Pfad vorbei durch den
226
+ // Command-Dispatcher. Beweist: (a) registry liegt in den deps,
227
+ // (b) dispatchSystemWrite geht durch Zod + Access-Check des Handlers,
228
+ // (c) der SystemUser trägt den Ziel-Tenant (Event-Store-Konsistenz).
229
+ let registryHasProbe = false;
230
+ const handle = await boot(undefined, {
231
+ extraRoutes: (app, deps) => {
232
+ registryHasProbe = deps.registry.features.has("prod-probe");
233
+ app.post("/webhook-probe", async (c) => {
234
+ const result = await deps.dispatchSystemWrite({
235
+ handlerQn: "prod-probe:write:probe-write",
236
+ payload: { note: "from-webhook" },
237
+ tenantId: TENANT_ID as import("@cosmicdrift/kumiko-framework/engine").TenantId,
238
+ });
239
+ return c.json(result);
240
+ });
241
+ },
242
+ });
243
+
244
+ expect(registryHasProbe).toBe(true);
245
+
246
+ const res = await handle.fetch(new Request("http://test/webhook-probe", { method: "POST" }));
247
+ expect(res.status).toBe(200);
248
+ const body = (await res.json()) as {
249
+ isSuccess: boolean;
250
+ data?: { tenantSeen: string; roles: string[] };
251
+ };
252
+ expect(body.isSuccess).toBe(true);
253
+ expect(body.data?.tenantSeen).toBe(TENANT_ID);
254
+ expect(body.data?.roles).toContain("SystemAdmin");
255
+ });
256
+
209
257
  test("static-fallback: extraRoute beats Disk-File at colliding path (Hono-First)", async () => {
210
258
  // Regression-Test für den static-fallback-Bug von Phase 2 Step 1:
211
259
  // wenn ein extraRoute (z.B. /feed.xml) UND eine gleichnamige Disk-
@@ -20,7 +20,11 @@ import { readFile, watch } from "node:fs/promises";
20
20
  import { tmpdir } from "node:os";
21
21
  import { join, resolve } from "node:path";
22
22
  import { type AuthRoutesConfig, generateToken } from "@cosmicdrift/kumiko-framework/api";
23
- import { buildAppSchema, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
23
+ import {
24
+ buildAppSchema,
25
+ createSystemUser,
26
+ type FeatureDefinition,
27
+ } from "@cosmicdrift/kumiko-framework/engine";
24
28
  import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
25
29
  import {
26
30
  pushEntityProjectionTables,
@@ -190,7 +194,21 @@ export type CreateKumikoServerOptions = {
190
194
  * `ctx` weil dies kein HandlerContext ist — kein user/tenant. */
191
195
  readonly extraRoutes?: (
192
196
  app: import("hono").Hono,
193
- deps: { db: TestStack["db"]; redis: TestStack["redis"] },
197
+ deps: {
198
+ db: TestStack["db"];
199
+ redis: TestStack["redis"];
200
+ /** Feature-registry — z.B. für Plugin-Lookups via
201
+ * `registry.getExtensionUsages("subscriptionProvider")`. */
202
+ registry: TestStack["registry"];
203
+ /** System-Write durch den Command-Dispatcher — Semantik identisch zu
204
+ * runProdApp.extraRoutes (SystemAdmin des Ziel-Tenants, kein
205
+ * Access-Check; nur für signatur-authentifizierte Pfade). */
206
+ dispatchSystemWrite: (args: {
207
+ readonly handlerQn: string;
208
+ readonly payload: unknown;
209
+ readonly tenantId: import("@cosmicdrift/kumiko-framework/engine").TenantId;
210
+ }) => Promise<import("@cosmicdrift/kumiko-framework/engine").WriteResult>;
211
+ },
194
212
  ) => void;
195
213
  };
196
214
 
@@ -681,7 +699,13 @@ export async function createKumikoServer(
681
699
  // (HTML/JS/CSS-Serving via handleFetch unten) registriert, damit
682
700
  // explizite Routen wie /feed.xml den Asset-Pfad schlagen.
683
701
  if (options.extraRoutes !== undefined) {
684
- options.extraRoutes(stack.app, { db: stack.db, redis: stack.redis });
702
+ options.extraRoutes(stack.app, {
703
+ db: stack.db,
704
+ redis: stack.redis,
705
+ registry: stack.registry,
706
+ dispatchSystemWrite: ({ handlerQn, payload, tenantId }) =>
707
+ stack.dispatcher.write(handlerQn, payload, createSystemUser(tenantId, ["SystemAdmin"])),
708
+ });
685
709
  }
686
710
 
687
711
  // setupTestStack konfiguriert den eventDispatcher, startet ihn aber
@@ -45,12 +45,15 @@ import { createDbConnection, type DbRunner } from "@cosmicdrift/kumiko-framework
45
45
  import {
46
46
  buildAppSchema,
47
47
  createRegistry,
48
+ createSystemUser,
48
49
  type EffectiveFeaturesResolver,
49
50
  type FeatureDefinition,
50
51
  findTierResolverUsage,
52
+ type Registry,
51
53
  type TenantId,
52
54
  type TierResolverPlugin,
53
55
  validateBoot,
56
+ type WriteResult,
54
57
  } from "@cosmicdrift/kumiko-framework/engine";
55
58
  import {
56
59
  type ApiEntrypoint,
@@ -421,6 +424,19 @@ export type RunProdAppOptions = {
421
424
  deps: {
422
425
  db: import("@cosmicdrift/kumiko-framework/db").DbConnection;
423
426
  redis: import("ioredis").default;
427
+ /** Feature-registry — z.B. für Plugin-Lookups via
428
+ * `registry.getExtensionUsages("subscriptionProvider")`. */
429
+ registry: Registry;
430
+ /** Schreibt durch den /api/*-Command-Dispatcher (gleiche Idempotency/
431
+ * Job-Hooks) — aber als auto-konstruierter SystemAdmin des Ziel-
432
+ * Tenants, OHNE Access-Check der Route. Nur für Pfade, die ihre
433
+ * Authentizität selbst beweisen (Provider-Webhook-Signaturen,
434
+ * createSubscriptionWebhookHandler et al.). */
435
+ dispatchSystemWrite: (args: {
436
+ readonly handlerQn: string;
437
+ readonly payload: unknown;
438
+ readonly tenantId: TenantId;
439
+ }) => Promise<WriteResult>;
424
440
  },
425
441
  ) => void;
426
442
  /** When true (default), Bun.serve is started before runProdApp resolves —
@@ -808,7 +824,17 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
808
824
  // belegt; extraRoutes sollte die nicht überschreiben (kein
809
825
  // enforce, das ist Author-Verantwortung).
810
826
  if (options.extraRoutes) {
811
- options.extraRoutes(entrypoint.app, { db, redis });
827
+ options.extraRoutes(entrypoint.app, {
828
+ db,
829
+ redis,
830
+ registry,
831
+ dispatchSystemWrite: ({ handlerQn, payload, tenantId }) =>
832
+ entrypoint.dispatcher.write(
833
+ handlerQn,
834
+ payload,
835
+ createSystemUser(tenantId, ["SystemAdmin"]),
836
+ ),
837
+ });
812
838
  }
813
839
 
814
840
  // 11. Build the fetch-handler. Static-fallback for non-/api/ paths