@cosmicdrift/kumiko-dev-server 0.3.0 → 0.4.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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,82 @@
|
|
|
1
1
|
# @cosmicdrift/kumiko-dev-server
|
|
2
2
|
|
|
3
|
+
## 0.4.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 010b410: feat(auth-email-password): "Bestätigungs-Mail erneut senden" im LoginScreen
|
|
8
|
+
|
|
9
|
+
LoginScreen bietet bei reason=email_not_verified jetzt einen Resend-Link
|
|
10
|
+
im Fehler-Banner — der existierende `requestEmailVerification`-Endpoint
|
|
11
|
+
wird direkt aufgerufen, der Banner wechselt nach Erfolg zum Info-Variant
|
|
12
|
+
("Wir haben dir eine neue Bestätigungs-Mail geschickt.").
|
|
13
|
+
|
|
14
|
+
UX-Details:
|
|
15
|
+
|
|
16
|
+
- Bei 429 → inline-Hint "Bitte warte kurz und versuche es erneut."
|
|
17
|
+
- Bei Netzwerk/sonstigen Fehlern → inline-Hint "Konnte nicht senden."
|
|
18
|
+
- Anti-Typo-Gate: ändert der User die Email-Eingabe nach dem Login-Fail,
|
|
19
|
+
verschwindet der Resend-Link — sonst würde Resend silent-success an die
|
|
20
|
+
geänderte (potentiell typoed) Adresse gehen ohne User-Feedback.
|
|
21
|
+
- Andere Failure-Codes (invalid_credentials etc.) zeigen weiterhin keinen
|
|
22
|
+
Resend-Link.
|
|
23
|
+
|
|
24
|
+
i18n: 4 neue Keys (DE+EN) im `auth.login.resend*`-Namespace, additive.
|
|
25
|
+
Apps die ihre Translations override-en müssen nichts ändern.
|
|
26
|
+
|
|
27
|
+
Additive UI-Feature — keine API-Breaks, keine Schema-Migration.
|
|
28
|
+
|
|
29
|
+
- Updated dependencies [010b410]
|
|
30
|
+
- @cosmicdrift/kumiko-framework@0.4.1
|
|
31
|
+
- @cosmicdrift/kumiko-bundled-features@0.4.1
|
|
32
|
+
|
|
33
|
+
## 0.4.0
|
|
34
|
+
|
|
35
|
+
### Minor Changes
|
|
36
|
+
|
|
37
|
+
- 825e7d2: Visual-Tree V.1.4 → V.1.6 — Feature-complete Editor + Folder-Hierarchy + Roving-tabindex.
|
|
38
|
+
|
|
39
|
+
**V.1.4** — explicit `folder?: string` Schema-Field auf text-block-entity. Slug bleibt
|
|
40
|
+
kebab-only validiert, Folder explizit gesetzt. Tree gruppiert via `groupBlocksByFolder`
|
|
41
|
+
(ersetzt `groupBlocksBySlugPrefix`). `Subscribe<T>` Signature um optional `emitError`
|
|
42
|
+
erweitert für explicit async-error-Pfade. ProviderBranch zeigt Error-Banner mit
|
|
43
|
+
Retry-Button. Drift-Test pinnt seedTextBlock-vs-set.write Slug-Validation.
|
|
44
|
+
|
|
45
|
+
**V.1.4b** — URL-State-Routing für Editor-Target via `nav.searchParams`. F5 + Back-Button
|
|
46
|
+
stellen den Editor-State wieder her. Format: `?t=text-content:edit&a_slug=...&a_lang=...`.
|
|
47
|
+
Plus `useDispatchTarget` hook ersetzt globalen `dispatchTarget` als empfohlenen Production-
|
|
48
|
+
Pfad (legacy bleibt für Test-Hooks).
|
|
49
|
+
|
|
50
|
+
**V.1.5** — Arrow-Key-Navigation (`<aside role="tree">`, ARIA-tree-Pattern) + SSE-driven
|
|
51
|
+
Tree-Refresh. `ClientFeatureDefinition.treeEntities?: string[]` listet Entity-Namen pro
|
|
52
|
+
Provider; live-events triggern provider-re-mount → Stale-Tree-state="stub"→"filled"
|
|
53
|
+
flippt nach save automatisch.
|
|
54
|
+
|
|
55
|
+
**V.1.5c+d** — Active-Node-Highlight (explicit blue + 2px border-l + scrollIntoView),
|
|
56
|
+
VS-Code-Polish (compact spacing, focus-visible, folder-icon-color text-amber, indent-
|
|
57
|
+
guides per ancestor-depth), Folder-Wrapper für legal-pages ("📁 Legal" + slug-first
|
|
58
|
+
Verschachtelung) und text-content ("📁 Content").
|
|
59
|
+
|
|
60
|
+
**V.1.6** — Multi-level Folder-Splitting (`folder="page/marketing"` → nested folders,
|
|
61
|
+
walk-or-create-pattern, folder/leaf-collision-tolerant). Roving-tabindex (nur focused-
|
|
62
|
+
treeitem hat tabIndex=0, Tab cyclt aus dem Tree raus).
|
|
63
|
+
|
|
64
|
+
35/35 kumiko check PASS, 13/13 group-blocks + 22/22 text-content integration tests grün.
|
|
65
|
+
Browser + Keyboard lokal validated.
|
|
66
|
+
|
|
67
|
+
**Breaking**: `TreeContext` Type entfernt (V.1.2 SR2-Rip — war nie genutzt). Provider sind
|
|
68
|
+
session-bound: `TreeChildrenSubscribe = () => Subscribe<T>` statt `(ctx) => Subscribe<T>`.
|
|
69
|
+
|
|
70
|
+
**V.1.7-Followups**: useEffect-deps in VisualTree-focus-init (Performance), Cancellation-
|
|
71
|
+
Token in TreeProvider's fetch (emit-after-unmount-warning), inline-rename, drag-drop,
|
|
72
|
+
file-icons per slug-extension, parent-jump bei ArrowLeft auf collapsed-item.
|
|
73
|
+
|
|
74
|
+
### Patch Changes
|
|
75
|
+
|
|
76
|
+
- Updated dependencies [825e7d2]
|
|
77
|
+
- @cosmicdrift/kumiko-framework@0.4.0
|
|
78
|
+
- @cosmicdrift/kumiko-bundled-features@0.4.0
|
|
79
|
+
|
|
3
80
|
## 0.3.0
|
|
4
81
|
|
|
5
82
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-dev-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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>",
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
"kumiko-dev": "./bin/kumiko-dev.ts"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@cosmicdrift/kumiko-bundled-features": "0.
|
|
52
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
51
|
+
"@cosmicdrift/kumiko-bundled-features": "0.4.1",
|
|
52
|
+
"@cosmicdrift/kumiko-framework": "0.4.1"
|
|
53
53
|
},
|
|
54
54
|
"publishConfig": {
|
|
55
55
|
"registry": "https://registry.npmjs.org",
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Pins the boot-wiring contract for config-seeds. runDevApp's onAfterSetup
|
|
2
|
+
// and runProdApp's seed-block both call `applyBootSeeds(...)` — this test
|
|
3
|
+
// calls the SAME helper, so if someone removes the call site from
|
|
4
|
+
// runDevApp / runProdApp the helper still has at least one caller (this
|
|
5
|
+
// test). Code review then sees an orphaned helper, not a silently broken
|
|
6
|
+
// boot. For a stricter end-to-end pin you'd start an actual runDevApp;
|
|
7
|
+
// that's heavy and not done here.
|
|
8
|
+
//
|
|
9
|
+
// Tests:
|
|
10
|
+
// 1. seed rows land in the projection after applyBootSeeds runs,
|
|
11
|
+
// 2. a re-boot is a no-op (idempotent),
|
|
12
|
+
// 3. an admin set on top of a seed wins the resolver cascade; coexistence
|
|
13
|
+
// vs. override semantics depend on the admin user's tenantId.
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
configValuesTable,
|
|
17
|
+
createConfigAccessorFactory,
|
|
18
|
+
createConfigFeature,
|
|
19
|
+
createConfigResolver,
|
|
20
|
+
} from "@cosmicdrift/kumiko-bundled-features/config";
|
|
21
|
+
import {
|
|
22
|
+
access,
|
|
23
|
+
createSystemConfig,
|
|
24
|
+
createSystemSeed,
|
|
25
|
+
createTenantConfig,
|
|
26
|
+
createTenantSeed,
|
|
27
|
+
defineFeature,
|
|
28
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
29
|
+
import {
|
|
30
|
+
setupTestStack,
|
|
31
|
+
type TestStack,
|
|
32
|
+
TestUsers,
|
|
33
|
+
unsafePushTables,
|
|
34
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
35
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
36
|
+
import { applyBootSeeds } from "../boot/apply-boot-seeds";
|
|
37
|
+
|
|
38
|
+
const bootSeedsFeature = defineFeature("boot-seeds-test", (r) => {
|
|
39
|
+
r.requires("config");
|
|
40
|
+
r.config({
|
|
41
|
+
keys: {
|
|
42
|
+
siteName: createTenantConfig("text", {
|
|
43
|
+
default: "DEFAULT_SITE",
|
|
44
|
+
read: access.all,
|
|
45
|
+
write: access.all,
|
|
46
|
+
}),
|
|
47
|
+
maintenance: createSystemConfig("boolean", {
|
|
48
|
+
default: false,
|
|
49
|
+
read: access.all,
|
|
50
|
+
write: access.systemAdmin,
|
|
51
|
+
}),
|
|
52
|
+
},
|
|
53
|
+
seeds: {
|
|
54
|
+
siteName: createTenantSeed({ value: "from-seed" }),
|
|
55
|
+
maintenance: createSystemSeed({ value: true }),
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const SITE_KEY = "boot-seeds-test:config:site-name";
|
|
61
|
+
const MAINT_KEY = "boot-seeds-test:config:maintenance";
|
|
62
|
+
|
|
63
|
+
let stack: TestStack;
|
|
64
|
+
const resolver = createConfigResolver();
|
|
65
|
+
|
|
66
|
+
beforeAll(async () => {
|
|
67
|
+
stack = await setupTestStack({
|
|
68
|
+
features: [createConfigFeature(), bootSeedsFeature],
|
|
69
|
+
extraContext: ({ registry }) => ({
|
|
70
|
+
configResolver: resolver,
|
|
71
|
+
_configAccessorFactory: createConfigAccessorFactory(registry, resolver),
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
74
|
+
await unsafePushTables(stack.db, { configValuesTable });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
afterAll(async () => {
|
|
78
|
+
await stack.cleanup();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("config-seed boot wiring", () => {
|
|
82
|
+
test("first boot: applyBootSeeds writes one row per seed", async () => {
|
|
83
|
+
await applyBootSeeds({ registry: stack.registry, db: stack.db });
|
|
84
|
+
|
|
85
|
+
const rows = await stack.db.select().from(configValuesTable);
|
|
86
|
+
expect(rows.length).toBe(2);
|
|
87
|
+
|
|
88
|
+
const siteKeyDef = stack.registry.getConfigKey(SITE_KEY);
|
|
89
|
+
expect(siteKeyDef).toBeDefined();
|
|
90
|
+
const sitePeek = await resolver.get(
|
|
91
|
+
SITE_KEY,
|
|
92
|
+
siteKeyDef!,
|
|
93
|
+
TestUsers.systemAdmin.tenantId,
|
|
94
|
+
TestUsers.systemAdmin.id,
|
|
95
|
+
stack.db,
|
|
96
|
+
);
|
|
97
|
+
expect(sitePeek).toBe("from-seed");
|
|
98
|
+
|
|
99
|
+
const maintKeyDef = stack.registry.getConfigKey(MAINT_KEY);
|
|
100
|
+
expect(maintKeyDef).toBeDefined();
|
|
101
|
+
const maintPeek = await resolver.get(
|
|
102
|
+
MAINT_KEY,
|
|
103
|
+
maintKeyDef!,
|
|
104
|
+
TestUsers.systemAdmin.tenantId,
|
|
105
|
+
TestUsers.systemAdmin.id,
|
|
106
|
+
stack.db,
|
|
107
|
+
);
|
|
108
|
+
expect(maintPeek).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("re-boot: idempotent — every seed already on disk → no extra rows", async () => {
|
|
112
|
+
await applyBootSeeds({ registry: stack.registry, db: stack.db });
|
|
113
|
+
|
|
114
|
+
const rows = await stack.db.select().from(configValuesTable);
|
|
115
|
+
expect(rows.length).toBe(2);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("admin set on top of seed wins resolver — Re-Boot preserves admin", async () => {
|
|
119
|
+
// siteName is a TENANT-scope key. The seed writes a row under
|
|
120
|
+
// SYSTEM_TENANT_ID (= "for all tenants"). An admin on a real tenant
|
|
121
|
+
// writes a row under THAT tenantId — higher specificity. Both rows
|
|
122
|
+
// coexist; the resolver returns the more specific one.
|
|
123
|
+
//
|
|
124
|
+
// If the admin happens to write as SYSTEM_TENANT_ID (e.g. test user
|
|
125
|
+
// is the system-admin on the system-tenant), the admin write hits
|
|
126
|
+
// the seed-row directly and updates the same aggregate stream. Both
|
|
127
|
+
// paths end up with the admin value winning — the row-count
|
|
128
|
+
// assertion makes the path explicit.
|
|
129
|
+
await stack.http.writeOk(
|
|
130
|
+
"config:write:set",
|
|
131
|
+
{ key: SITE_KEY, value: "admin-override", scope: "tenant" },
|
|
132
|
+
TestUsers.systemAdmin,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
await applyBootSeeds({ registry: stack.registry, db: stack.db });
|
|
136
|
+
|
|
137
|
+
const siteKeyDef = stack.registry.getConfigKey(SITE_KEY);
|
|
138
|
+
expect(siteKeyDef).toBeDefined();
|
|
139
|
+
const peek = await resolver.get(
|
|
140
|
+
SITE_KEY,
|
|
141
|
+
siteKeyDef!,
|
|
142
|
+
TestUsers.systemAdmin.tenantId,
|
|
143
|
+
TestUsers.systemAdmin.id,
|
|
144
|
+
stack.db,
|
|
145
|
+
);
|
|
146
|
+
expect(peek).toBe("admin-override");
|
|
147
|
+
|
|
148
|
+
// Row-count tells us which path was hit:
|
|
149
|
+
// - 2 rows = override path (admin tenantId === SYSTEM_TENANT_ID,
|
|
150
|
+
// updated the seed-stream in place).
|
|
151
|
+
// - 3 rows = coexistence path (admin tenantId !== SYSTEM_TENANT_ID,
|
|
152
|
+
// new specific-tenant row sits next to the seed system-row).
|
|
153
|
+
// Either is correct as long as the resolver picks the admin value.
|
|
154
|
+
const rows = await stack.db.select().from(configValuesTable);
|
|
155
|
+
expect([2, 3]).toContain(rows.length);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { seedAllConfigValues } from "@cosmicdrift/kumiko-bundled-features/config";
|
|
2
|
+
import type { DbConnection, EncryptionProvider } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
+
import type { Registry } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
+
|
|
5
|
+
// Single boot-seed entry-point. runDevApp + runProdApp both call this
|
|
6
|
+
// from their post-stack hook, so the wiring lives in exactly one place
|
|
7
|
+
// — config-seed-boot.integration.ts pins this helper, which means a
|
|
8
|
+
// missing call site (e.g. someone deletes the line from runDevApp)
|
|
9
|
+
// surfaces as a missing-helper-use in code review rather than silently
|
|
10
|
+
// shipping a server that never seeds.
|
|
11
|
+
export async function applyBootSeeds(deps: {
|
|
12
|
+
registry: Registry;
|
|
13
|
+
db: DbConnection;
|
|
14
|
+
encryption?: EncryptionProvider;
|
|
15
|
+
}): Promise<void> {
|
|
16
|
+
await seedAllConfigValues(deps.registry, deps.db, deps.encryption);
|
|
17
|
+
}
|
package/src/run-dev-app.ts
CHANGED
|
@@ -24,7 +24,6 @@ import {
|
|
|
24
24
|
type SessionCallbacks,
|
|
25
25
|
} from "@cosmicdrift/kumiko-bundled-features/sessions";
|
|
26
26
|
import { TenantQueries } from "@cosmicdrift/kumiko-bundled-features/tenant";
|
|
27
|
-
|
|
28
27
|
import type { SessionMetadata } from "@cosmicdrift/kumiko-framework/api";
|
|
29
28
|
import {
|
|
30
29
|
type EffectiveFeaturesResolver,
|
|
@@ -35,6 +34,7 @@ import {
|
|
|
35
34
|
type TierResolverPlugin,
|
|
36
35
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
37
36
|
import type { TestStack } from "@cosmicdrift/kumiko-framework/stack";
|
|
37
|
+
import { applyBootSeeds } from "./boot/apply-boot-seeds";
|
|
38
38
|
|
|
39
39
|
import { watchAndRegenerate } from "./codegen";
|
|
40
40
|
import { buildComposeAuthOptions, composeFeatures } from "./compose-features";
|
|
@@ -325,6 +325,11 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
|
|
|
325
325
|
if (options.auth) {
|
|
326
326
|
await seedAdmin(stack.db, options.auth.admin);
|
|
327
327
|
}
|
|
328
|
+
// Apply r.config({ seeds }) declared by any registered feature.
|
|
329
|
+
// Runs before user-supplied seed callbacks so those can read /
|
|
330
|
+
// override the deploy-defaults. The helper indirection is what
|
|
331
|
+
// config-seed-boot.integration.ts pins — keep it as a single call.
|
|
332
|
+
await applyBootSeeds({ registry: stack.registry, db: stack.db });
|
|
328
333
|
for (const seed of options.seeds ?? []) {
|
|
329
334
|
await seed(stack);
|
|
330
335
|
}
|
package/src/run-prod-app.ts
CHANGED
|
@@ -65,6 +65,7 @@ import {
|
|
|
65
65
|
createIdempotencyGuard,
|
|
66
66
|
} from "@cosmicdrift/kumiko-framework/pipeline";
|
|
67
67
|
import Redis from "ioredis";
|
|
68
|
+
import { applyBootSeeds } from "./boot/apply-boot-seeds";
|
|
68
69
|
import { ASSETS_DIR } from "./build-prod-bundle";
|
|
69
70
|
import { buildComposeAuthOptions, composeFeatures } from "./compose-features";
|
|
70
71
|
import { injectSchema } from "./inject-schema";
|
|
@@ -589,13 +590,15 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
|
|
|
589
590
|
// statt eines fixed JSON-Strings. Heute: registry-static, also OK.
|
|
590
591
|
const appSchemaJson = JSON.stringify(buildAppSchema(registry));
|
|
591
592
|
|
|
592
|
-
// 9. Seeds: admin first, then
|
|
593
|
-
// idempotent — runProdApp doesn't gate
|
|
594
|
-
//
|
|
595
|
-
//
|
|
593
|
+
// 9. Seeds: admin first, then config-seeds from r.config({seeds}),
|
|
594
|
+
// then app-specific. All idempotent — runProdApp doesn't gate
|
|
595
|
+
// "first boot" via flag, every seed-step checks its own
|
|
596
|
+
// preconditions. Config-seeds rely on a deterministic
|
|
597
|
+
// aggregate-id so re-boot becomes a version_conflict skip.
|
|
596
598
|
if (options.auth) {
|
|
597
599
|
await seedAdmin(db, options.auth.admin);
|
|
598
600
|
}
|
|
601
|
+
await applyBootSeeds({ registry, db });
|
|
599
602
|
for (const seed of options.seeds ?? []) {
|
|
600
603
|
await seed({ db });
|
|
601
604
|
}
|