@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.
|
|
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 {
|
|
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: {
|
|
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, {
|
|
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
|
package/src/run-prod-app.ts
CHANGED
|
@@ -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, {
|
|
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
|