@cosmicdrift/kumiko-dev-server 0.1.0 → 0.2.1
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/LICENSE
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Business Source License 1.1
|
|
2
|
+
|
|
3
|
+
Parameters
|
|
4
|
+
|
|
5
|
+
Licensor: Marc Frost
|
|
6
|
+
|
|
7
|
+
Licensed Work: @cosmicdrift/kumiko-framework
|
|
8
|
+
The Licensed Work is © 2026 Marc Frost.
|
|
9
|
+
|
|
10
|
+
Additional Use Grant:
|
|
11
|
+
You may use the Licensed Work in production for any purpose, including
|
|
12
|
+
commercially, EXCEPT for the Restricted Use.
|
|
13
|
+
|
|
14
|
+
"Restricted Use" is defined as using the Licensed Work to provide a platform
|
|
15
|
+
or service to third parties that allows them to host, deploy, or run their
|
|
16
|
+
own applications built with the Licensed Work. This includes, but is not
|
|
17
|
+
limited to: managed hosting services, software-as-a-service (SaaS) platforms,
|
|
18
|
+
platform-as-a-service (PaaS), developer platforms, or any multi-tenant
|
|
19
|
+
managed offering of the Licensed Work.
|
|
20
|
+
|
|
21
|
+
This restriction does not apply to the Licensor, any entity controlled by,
|
|
22
|
+
controlling, or under common control with the Licensor ("Affiliates"), or
|
|
23
|
+
contractors acting on their behalf. The Licensor remains free to use the
|
|
24
|
+
Licensed Work for any purpose, including for the operation of kumiko.so.
|
|
25
|
+
|
|
26
|
+
Change Date: 2030-05-05
|
|
27
|
+
Change License: Apache License, Version 2.0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
Terms
|
|
31
|
+
|
|
32
|
+
The Licensor hereby grants you the right to copy, modify, create derivative works,
|
|
33
|
+
redistribute, and make non-production use of the Licensed Work. The Licensor may
|
|
34
|
+
make an Additional Use Grant, above, permitting limited production use.
|
|
35
|
+
|
|
36
|
+
Effective on the Change Date, or the fourth anniversary of the first publicly
|
|
37
|
+
available distribution of the Licensed Work under this License, whichever comes
|
|
38
|
+
first, this License will convert to the Change License.
|
|
39
|
+
|
|
40
|
+
This Business Source License governs use of the Licensed Work in all cases, except
|
|
41
|
+
as to any use that is explicitly granted in the Additional Use Grant above or
|
|
42
|
+
under the Change License after the Change Date.
|
|
43
|
+
|
|
44
|
+
If your use of the Licensed Work does not comply with the requirements of this
|
|
45
|
+
License, you must cease use of the Licensed Work immediately.
|
|
46
|
+
|
|
47
|
+
All copies of the Licensed Work, and all derivative works thereof, must include
|
|
48
|
+
this License.
|
|
49
|
+
|
|
50
|
+
This License does not grant you any right, title, or interest in any trademark,
|
|
51
|
+
logo, or branding of the Licensor, except as required to comply with this License.
|
|
52
|
+
|
|
53
|
+
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON AN
|
|
54
|
+
“AS IS” BASIS. LICENSOR DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING
|
|
55
|
+
WITHOUT LIMITATION WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
|
|
56
|
+
TITLE, AND NON-INFRINGEMENT. IN NO EVENT WILL LICENSOR BE LIABLE FOR ANY DAMAGES
|
|
57
|
+
ARISING OUT OF OR RELATED TO THIS LICENSE OR THE USE OF THE LICENSED WORK.
|
package/package.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-dev-server",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
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>",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/
|
|
9
|
+
"url": "git+https://github.com/CosmicDriftGameStudio/kumiko-framework.git",
|
|
10
10
|
"directory": "packages/dev-server"
|
|
11
11
|
},
|
|
12
12
|
"bugs": {
|
|
13
|
-
"url": "https://github.com/
|
|
13
|
+
"url": "https://github.com/CosmicDriftGameStudio/kumiko-framework/issues"
|
|
14
14
|
},
|
|
15
15
|
"homepage": "https://kumiko.so",
|
|
16
16
|
"type": "module",
|
|
@@ -36,11 +36,11 @@ import { seedTenantMembership } from "@cosmicdrift/kumiko-bundled-features/tenan
|
|
|
36
36
|
import { UserHandlers, userEntity, userTable } from "@cosmicdrift/kumiko-bundled-features/user";
|
|
37
37
|
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
38
38
|
import {
|
|
39
|
-
createEntityTable,
|
|
40
|
-
pushTables,
|
|
41
39
|
setupTestStack,
|
|
42
40
|
type TestStack,
|
|
43
41
|
TestUsers,
|
|
42
|
+
unsafeCreateEntityTable,
|
|
43
|
+
unsafePushTables,
|
|
44
44
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
45
45
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
46
46
|
import { composeFeatures } from "../compose-features";
|
|
@@ -110,9 +110,9 @@ async function bootStack(
|
|
|
110
110
|
},
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
-
await
|
|
114
|
-
await
|
|
115
|
-
await
|
|
113
|
+
await unsafeCreateEntityTable(stack.db, userEntity);
|
|
114
|
+
await unsafeCreateEntityTable(stack.db, tenantEntity);
|
|
115
|
+
await unsafePushTables(stack.db, { configValuesTable, tenantMembershipsTable });
|
|
116
116
|
|
|
117
117
|
return { stack, resetEmails, verifyEmails };
|
|
118
118
|
}
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
createEventConsumerStateTable,
|
|
28
28
|
createProjectionStateTable,
|
|
29
29
|
} from "@cosmicdrift/kumiko-framework/pipeline";
|
|
30
|
-
import {
|
|
30
|
+
import { unsafeEnsureEntityTable } from "@cosmicdrift/kumiko-framework/stack";
|
|
31
31
|
import { sql } from "drizzle-orm";
|
|
32
32
|
import postgres from "postgres";
|
|
33
33
|
import { afterEach, beforeAll, describe, expect, test } from "vitest";
|
|
@@ -116,7 +116,7 @@ async function migrateTestDb(): Promise<void> {
|
|
|
116
116
|
await createArchivedStreamsTable(db);
|
|
117
117
|
await createProjectionStateTable(db);
|
|
118
118
|
await createEventConsumerStateTable(db);
|
|
119
|
-
await
|
|
119
|
+
await unsafeEnsureEntityTable(db, widgetEntity, "widget");
|
|
120
120
|
} finally {
|
|
121
121
|
await close();
|
|
122
122
|
}
|
|
@@ -167,7 +167,7 @@ describe("runProdApp", () => {
|
|
|
167
167
|
test("second boot against the same DB is idempotent — no crash, no duplicate tables", async () => {
|
|
168
168
|
await boot();
|
|
169
169
|
// First boot left tables in place. Restart on the same DB —
|
|
170
|
-
//
|
|
170
|
+
// unsafeEnsureEntityTable should be a no-op for the existing rows.
|
|
171
171
|
const second = await boot();
|
|
172
172
|
|
|
173
173
|
const res = await second.entrypoint.app.fetch(new Request("http://test/health"));
|
|
@@ -23,7 +23,7 @@ import { type AuthRoutesConfig, generateToken } from "@cosmicdrift/kumiko-framew
|
|
|
23
23
|
import { buildAppSchema, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
24
24
|
import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
25
25
|
import {
|
|
26
|
-
|
|
26
|
+
pushEntityProjectionTables,
|
|
27
27
|
setupTestStack,
|
|
28
28
|
type TestStack,
|
|
29
29
|
type TestStackOptions,
|
|
@@ -170,8 +170,16 @@ export type CreateKumikoServerOptions = {
|
|
|
170
170
|
* Handler `roles: ["anonymous"]` deklariert. Tenant-Resolution per
|
|
171
171
|
* Header/Cookie/Default; siehe AnonymousAccessConfig. */
|
|
172
172
|
readonly anonymousAccess?: TestStackOptions["anonymousAccess"];
|
|
173
|
+
/** Feature-toggle resolver — durchgereicht an setupTestStack. Wenn
|
|
174
|
+
* gesetzt, konsultiert der dispatcher-feature-gate, hook-filter, MSP-
|
|
175
|
+
* filter den callback; absent = alle features always-on. Erforderlich
|
|
176
|
+
* für Tier-Composition (Sprint 8) wo per-Tenant unterschiedliche
|
|
177
|
+
* features aktiv sein sollen. Die typische produktive Implementierung
|
|
178
|
+
* ist `() => globalFeatureToggleRuntime.effectiveFeatures` post-boot
|
|
179
|
+
* (createLateBoundHolder-pattern, weil runtime stack.db braucht). */
|
|
180
|
+
readonly effectiveFeatures?: TestStackOptions["effectiveFeatures"];
|
|
173
181
|
/** Wird nach dem Aufsetzen der Entity-Tabellen aufgerufen. Hook für
|
|
174
|
-
* non-entity-tables (
|
|
182
|
+
* non-entity-tables (unsafePushTables) und Seeding (admin user, initial
|
|
175
183
|
* tenant, …). Muss idempotent sein — im persistent-DB-Modus läuft
|
|
176
184
|
* es bei jedem Boot. */
|
|
177
185
|
readonly onAfterSetup?: (stack: TestStack) => Promise<void>;
|
|
@@ -526,26 +534,6 @@ async function startTailwindWatcher(
|
|
|
526
534
|
};
|
|
527
535
|
}
|
|
528
536
|
|
|
529
|
-
// Create all entity tables declared by the given features. Uses
|
|
530
|
-
// ensureEntityTable so a persistent DB (KUMIKO_DEV_DB_NAME) can
|
|
531
|
-
// reuse tables from the previous boot without the caller having to
|
|
532
|
-
// check.
|
|
533
|
-
async function createEntityTablesForFeatures(
|
|
534
|
-
stack: TestStack,
|
|
535
|
-
features: readonly FeatureDefinition[],
|
|
536
|
-
): Promise<void> {
|
|
537
|
-
for (const feature of features) {
|
|
538
|
-
for (const [entityName, entity] of Object.entries(feature.entities)) {
|
|
539
|
-
const created = await ensureEntityTable(stack.db, entity, entityName);
|
|
540
|
-
if (!created) {
|
|
541
|
-
logInfo(
|
|
542
|
-
`[kumiko-server] table ${entity.table ?? entityName} already exists — skipping create`,
|
|
543
|
-
);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
549
537
|
/** @internal — normalisierte Client-Entry-Form, einheitlich über
|
|
550
538
|
* Single-Mode (`clientEntry`) und Multi-Mode (`clientEntries`). */
|
|
551
539
|
type NormalizedEntry = {
|
|
@@ -661,9 +649,12 @@ export async function createKumikoServer(
|
|
|
661
649
|
...(options.auth !== undefined && { authConfig: options.auth }),
|
|
662
650
|
...(options.extraContext !== undefined && { extraContext: options.extraContext }),
|
|
663
651
|
...(options.anonymousAccess !== undefined && { anonymousAccess: options.anonymousAccess }),
|
|
652
|
+
...(options.effectiveFeatures !== undefined && {
|
|
653
|
+
effectiveFeatures: options.effectiveFeatures,
|
|
654
|
+
}),
|
|
664
655
|
});
|
|
665
656
|
await createEventsTable(stack.db);
|
|
666
|
-
await
|
|
657
|
+
await pushEntityProjectionTables(stack, stack.registry);
|
|
667
658
|
|
|
668
659
|
// Hook für Caller-spezifische Tables + Seed. Läuft nach den Entity-
|
|
669
660
|
// Tabellen damit das Sample auf `stack.db` / `stack.dispatcher`
|
package/src/run-dev-app.ts
CHANGED
|
@@ -26,7 +26,14 @@ import {
|
|
|
26
26
|
import { TenantQueries } from "@cosmicdrift/kumiko-bundled-features/tenant";
|
|
27
27
|
|
|
28
28
|
import type { SessionMetadata } from "@cosmicdrift/kumiko-framework/api";
|
|
29
|
-
import
|
|
29
|
+
import {
|
|
30
|
+
type EffectiveFeaturesResolver,
|
|
31
|
+
type FeatureDefinition,
|
|
32
|
+
findTierResolverUsage,
|
|
33
|
+
type SessionUser,
|
|
34
|
+
type TenantId,
|
|
35
|
+
type TierResolverPlugin,
|
|
36
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
30
37
|
import type { TestStack } from "@cosmicdrift/kumiko-framework/stack";
|
|
31
38
|
|
|
32
39
|
import { watchAndRegenerate } from "./codegen";
|
|
@@ -138,6 +145,13 @@ export type RunDevAppOptions = {
|
|
|
138
145
|
* Hono-app gehängt, läuft VOR dem static-asset-Pfad. Symmetrisch zur
|
|
139
146
|
* gleichnamigen Option in runProdApp. */
|
|
140
147
|
readonly extraRoutes?: CreateKumikoServerOptions["extraRoutes"];
|
|
148
|
+
/** Feature-toggle resolver — durchgereicht an createKumikoServer →
|
|
149
|
+
* setupTestStack. Sprint-8 Tier-Composition: per-Tenant unterschied-
|
|
150
|
+
* liche features aktiv via globalFeatureToggleRuntime. Pattern in
|
|
151
|
+
* bin/server.ts: createLateBoundHolder + post-boot runtime.initialize
|
|
152
|
+
* in einem seed-fn, weil die runtime stack.db braucht und die seed-
|
|
153
|
+
* Funktionen nach setupTestStack laufen. */
|
|
154
|
+
readonly effectiveFeatures?: CreateKumikoServerOptions["effectiveFeatures"];
|
|
141
155
|
};
|
|
142
156
|
|
|
143
157
|
export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServerHandle> {
|
|
@@ -163,6 +177,33 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
|
|
|
163
177
|
...(composeAuthOptions && { authOptions: composeAuthOptions }),
|
|
164
178
|
});
|
|
165
179
|
|
|
180
|
+
// Sprint-8a Tier-Composition auto-wire: scan features for a
|
|
181
|
+
// tenantTierResolver-extension. If found AND user didn't supply own
|
|
182
|
+
// effectiveFeatures, we wire a late-bound wrapper here and fill it
|
|
183
|
+
// in onAfterSetup (where stack.db + stack.registry are available).
|
|
184
|
+
// App-Author sees nothing — `createTierEngineFeature(opts)` mounts +
|
|
185
|
+
// framework auto-wires.
|
|
186
|
+
const tierResolverUsage = options.effectiveFeatures ? undefined : findTierResolverUsage(features);
|
|
187
|
+
const tierResolverHolder: { resolver: EffectiveFeaturesResolver | undefined } = {
|
|
188
|
+
resolver: undefined,
|
|
189
|
+
};
|
|
190
|
+
const finalEffectiveFeatures: EffectiveFeaturesResolver | undefined =
|
|
191
|
+
options.effectiveFeatures ??
|
|
192
|
+
(tierResolverUsage
|
|
193
|
+
? (tenantId: TenantId) => {
|
|
194
|
+
// Defensive: Server starts AFTER onAfterSetup completes, so the
|
|
195
|
+
// resolver is filled before any request comes in. Throwing here
|
|
196
|
+
// means a programming error (boot order) rather than silent
|
|
197
|
+
// "all-features-on" misbehavior.
|
|
198
|
+
if (!tierResolverHolder.resolver) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
"tier-resolver: extension found but resolver not yet built — boot order issue?",
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
return tierResolverHolder.resolver(tenantId);
|
|
204
|
+
}
|
|
205
|
+
: undefined);
|
|
206
|
+
|
|
166
207
|
// configResolver-default fürs config-feature — im auth-mode immer
|
|
167
208
|
// hinzufügen, im no-auth-mode dem Caller überlassen. Factory-form
|
|
168
209
|
// wird gewrap't damit der spread auf das aufgerufene Result greift,
|
|
@@ -216,6 +257,9 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
|
|
|
216
257
|
...(extraContext !== undefined && { extraContext }),
|
|
217
258
|
...(options.anonymousAccess !== undefined && { anonymousAccess: options.anonymousAccess }),
|
|
218
259
|
...(options.extraRoutes !== undefined && { extraRoutes: options.extraRoutes }),
|
|
260
|
+
...(finalEffectiveFeatures !== undefined && {
|
|
261
|
+
effectiveFeatures: finalEffectiveFeatures,
|
|
262
|
+
}),
|
|
219
263
|
...(options.auth && {
|
|
220
264
|
auth: {
|
|
221
265
|
membershipQuery: TenantQueries.memberships,
|
|
@@ -261,6 +305,16 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
|
|
|
261
305
|
},
|
|
262
306
|
}),
|
|
263
307
|
onAfterSetup: async (stack) => {
|
|
308
|
+
// Sprint-8a: build tier-resolver BEFORE any seeds so seeds can rely
|
|
309
|
+
// on the resolver being live (e.g. seed that writes a SystemAdmin's
|
|
310
|
+
// tier-assignment can immediately read tier-cuts).
|
|
311
|
+
if (tierResolverUsage) {
|
|
312
|
+
const plugin = tierResolverUsage.options as TierResolverPlugin;
|
|
313
|
+
tierResolverHolder.resolver = await plugin.build({
|
|
314
|
+
db: stack.db,
|
|
315
|
+
registry: stack.registry,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
264
318
|
if (options.auth?.sessions !== undefined) {
|
|
265
319
|
const expiresInMs = options.auth.sessions.expiresInMs;
|
|
266
320
|
sessionCallbacks = createSessionCallbacks({
|
package/src/run-prod-app.ts
CHANGED
|
@@ -46,7 +46,11 @@ import { createDbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
|
46
46
|
import {
|
|
47
47
|
buildAppSchema,
|
|
48
48
|
createRegistry,
|
|
49
|
+
type EffectiveFeaturesResolver,
|
|
49
50
|
type FeatureDefinition,
|
|
51
|
+
findTierResolverUsage,
|
|
52
|
+
type TenantId,
|
|
53
|
+
type TierResolverPlugin,
|
|
50
54
|
validateBoot,
|
|
51
55
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
52
56
|
import {
|
|
@@ -351,6 +355,12 @@ export type RunProdAppOptions = {
|
|
|
351
355
|
* the fetch-handler directly (Bun.serve isn't available under vitest +
|
|
352
356
|
* node), then call handle.listen() manually if needed. */
|
|
353
357
|
readonly autoListen?: boolean;
|
|
358
|
+
/** Feature-toggle resolver — durchgereicht an createApiEntrypoint's
|
|
359
|
+
* dispatcherOptions. Sprint-8 Tier-Composition: per-Tenant unterschied-
|
|
360
|
+
* liche features aktiv via globalFeatureToggleRuntime. Pattern:
|
|
361
|
+
* createLateBoundHolder + post-boot runtime.initialize in einem
|
|
362
|
+
* seed-fn (db ist erst nach migrations + features ready). */
|
|
363
|
+
readonly effectiveFeatures?: (tenantId: TenantId) => ReadonlySet<string>;
|
|
354
364
|
};
|
|
355
365
|
|
|
356
366
|
export type ProdAppHandle = {
|
|
@@ -403,6 +413,20 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
|
|
|
403
413
|
validateBoot(features);
|
|
404
414
|
const registry = createRegistry(features);
|
|
405
415
|
|
|
416
|
+
// Sprint-8a Tier-Composition auto-wire: scan features for a
|
|
417
|
+
// tenantTierResolver-extension. If found AND user didn't supply own
|
|
418
|
+
// effectiveFeatures, build the resolver here (db + registry are
|
|
419
|
+
// available) before the dispatcher is constructed. App-Author sees
|
|
420
|
+
// nothing — `createTierEngineFeature(opts)` mounts + framework auto-wires.
|
|
421
|
+
let resolvedEffectiveFeatures: EffectiveFeaturesResolver | undefined = options.effectiveFeatures;
|
|
422
|
+
if (resolvedEffectiveFeatures === undefined) {
|
|
423
|
+
const tierResolverUsage = findTierResolverUsage(features);
|
|
424
|
+
if (tierResolverUsage) {
|
|
425
|
+
const plugin = tierResolverUsage.options as TierResolverPlugin;
|
|
426
|
+
resolvedEffectiveFeatures = await plugin.build({ db, registry });
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
406
430
|
// 5. Schema-Drift-Gate. Drizzle-kit migrate (yarn kumiko migrate apply)
|
|
407
431
|
// läuft als CI-Step VOR dem Container-Rollout. Boot prüft hier nur:
|
|
408
432
|
// (a) Alle Migrations aus drizzle/migrations/meta/_journal.json
|
|
@@ -486,7 +510,10 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
|
|
|
486
510
|
jwtSecret,
|
|
487
511
|
...(jwtIssuer && { jwtIssuer }),
|
|
488
512
|
...(instanceId && { instanceId }),
|
|
489
|
-
dispatcherOptions: {
|
|
513
|
+
dispatcherOptions: {
|
|
514
|
+
idempotency,
|
|
515
|
+
...(resolvedEffectiveFeatures && { effectiveFeatures: resolvedEffectiveFeatures }),
|
|
516
|
+
},
|
|
490
517
|
eventDedup,
|
|
491
518
|
...(options.auth && {
|
|
492
519
|
auth: {
|