@cosmicdrift/kumiko-dev-server 1.0.0 → 2.0.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/bin/kumiko-schema-check.ts +7 -0
- package/package.json +12 -7
- package/src/__tests__/build-prod-bundle.integration.test.ts +1 -1
- package/src/__tests__/build-server-bundle.test.ts +62 -0
- package/src/__tests__/compose-stacks.test.ts +271 -0
- package/src/__tests__/create-kumiko-server-errors.test.ts +54 -0
- package/src/__tests__/create-kumiko-server-options.test.ts +33 -0
- package/src/__tests__/create-kumiko-server.integration.test.ts +128 -1
- package/src/__tests__/discover-format.test.ts +1 -1
- package/src/__tests__/env-schema.integration.test.ts +1 -1
- package/src/__tests__/few-shot-corpus.test.ts +16 -11
- package/src/__tests__/merge-extra-context.test.ts +56 -0
- package/src/__tests__/resolve-stylesheet.test.ts +16 -0
- package/src/__tests__/saas-identity-wire.integration.test.ts +430 -0
- package/src/__tests__/scaffold-app-feature.test.ts +59 -6
- package/src/__tests__/scaffold-app.test.ts +34 -2
- package/src/__tests__/schema-apply.integration.test.ts +5 -2
- package/src/__tests__/setup-test-stack-from-features.integration.test.ts +30 -0
- package/src/__tests__/walkthrough.integration.test.ts +22 -6
- package/src/build-server-bundle.ts +33 -15
- package/src/build.ts +1 -2
- package/src/codegen/__tests__/render-codegen.test.ts +2 -1
- package/src/codegen/__tests__/run-codegen.test.ts +2 -2
- package/src/codegen/__tests__/strict-mode-diagnostics.test.ts +6 -1
- package/src/codegen/__tests__/watch.test.ts +1 -2
- package/src/codegen/render.ts +6 -1
- package/src/codegen/scan-events.ts +7 -5
- package/src/codegen/watch.ts +7 -4
- package/src/compose-stacks.ts +184 -0
- package/src/create-kumiko-server.ts +28 -5
- package/src/few-shot-corpus.ts +4 -3
- package/src/index.ts +30 -12
- package/src/run-dev-app.ts +195 -61
- package/src/scaffold-app-feature.ts +118 -15
- package/src/scaffold-app.ts +151 -79
- package/src/scaffold-demo-tasks.ts +233 -0
- package/src/schema-apply.ts +15 -2
- package/src/schema-check-core.ts +1 -1
- package/src/setup-test-stack-from-features.ts +61 -0
- package/src/welcome-banner.ts +1 -4
- package/src/__tests__/boot-extra-context.test.ts +0 -140
- package/src/__tests__/build-prod-bundle.test.ts +0 -311
- package/src/__tests__/cache-headers.test.ts +0 -83
- package/src/__tests__/compose-features-wiring.integration.test.ts +0 -382
- package/src/__tests__/compose-features.test.ts +0 -129
- package/src/__tests__/config-seed-boot.integration.test.ts +0 -158
- package/src/__tests__/inject-schema.test.ts +0 -62
- package/src/__tests__/renderer-web-css-relocation.integration.test.ts +0 -85
- package/src/__tests__/renderer-web-shell-sentinel.test.ts +0 -35
- package/src/__tests__/require-env.test.ts +0 -29
- package/src/__tests__/resolve-auth-mail.test.ts +0 -69
- package/src/__tests__/resolve-tailwind-cli.test.ts +0 -81
- package/src/__tests__/run-prod-app-env-source.test.ts +0 -157
- package/src/__tests__/run-prod-app-spec.test.ts +0 -57
- package/src/__tests__/run-prod-app.integration.test.ts +0 -840
- package/src/__tests__/session-wiring.test.ts +0 -51
- package/src/__tests__/try-hono-first.test.ts +0 -63
- package/src/boot/apply-boot-seeds.ts +0 -18
- package/src/build-prod-bundle.ts +0 -697
- package/src/compose-features.ts +0 -145
- package/src/extra-routes-deps.ts +0 -47
- package/src/inject-schema.ts +0 -24
- package/src/resolve-tailwind-cli.ts +0 -45
- package/src/run-prod-app.ts +0 -1492
- package/src/session-wiring.ts +0 -29
- package/src/try-hono-first.ts +0 -46
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// Demo `tasks` feature + seed for scaffolded apps — wasp-like starter UX.
|
|
2
|
+
// `createDemoTasksFeature()` feeds init-migration generation; the render*
|
|
3
|
+
// functions emit the same shape into the user's repo.
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
createBooleanField,
|
|
7
|
+
createEntity,
|
|
8
|
+
createNumberField,
|
|
9
|
+
createTextField,
|
|
10
|
+
defineEntityCreateHandler,
|
|
11
|
+
defineEntityDeleteHandler,
|
|
12
|
+
defineEntityDetailHandler,
|
|
13
|
+
defineEntityListHandler,
|
|
14
|
+
defineEntityUpdateHandler,
|
|
15
|
+
defineFeature,
|
|
16
|
+
type FeatureDefinition,
|
|
17
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
18
|
+
import type {
|
|
19
|
+
EntityEditScreenDefinition,
|
|
20
|
+
EntityListScreenDefinition,
|
|
21
|
+
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
22
|
+
|
|
23
|
+
const taskEntity = createEntity({
|
|
24
|
+
fields: {
|
|
25
|
+
title: createTextField({ required: true, sortable: true }),
|
|
26
|
+
status: createTextField({ sortable: true }),
|
|
27
|
+
priority: createNumberField(),
|
|
28
|
+
isUrgent: createBooleanField({ default: false }),
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const listScreen: EntityListScreenDefinition = {
|
|
33
|
+
id: "task-list",
|
|
34
|
+
type: "entityList",
|
|
35
|
+
entity: "task",
|
|
36
|
+
columns: ["title", "status", "isUrgent", "priority"],
|
|
37
|
+
defaultSort: { field: "title", dir: "asc" },
|
|
38
|
+
rowActions: [{ kind: "navigate", id: "edit", label: "Edit", screen: "task-edit" }],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const editScreen: EntityEditScreenDefinition = {
|
|
42
|
+
id: "task-edit",
|
|
43
|
+
type: "entityEdit",
|
|
44
|
+
entity: "task",
|
|
45
|
+
layout: {
|
|
46
|
+
sections: [{ title: "Task", fields: ["title", "status", "priority", "isUrgent"] }],
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const open = { access: { openToAll: true } } as const;
|
|
51
|
+
|
|
52
|
+
const TASKS_I18N = {
|
|
53
|
+
"screen:task-list.title": { de: "Aufgaben", en: "Tasks" },
|
|
54
|
+
"screen:task-edit.title": { de: "Aufgabe", en: "Task" },
|
|
55
|
+
"tasks:entity:task:field:title": { de: "Titel", en: "Title" },
|
|
56
|
+
"tasks:entity:task:field:status": { de: "Status", en: "Status" },
|
|
57
|
+
"tasks:entity:task:field:priority": { de: "Priorität", en: "Priority" },
|
|
58
|
+
"tasks:entity:task:field:isUrgent": { de: "Dringend", en: "Urgent" },
|
|
59
|
+
} as const;
|
|
60
|
+
|
|
61
|
+
/** Canonical demo feature — keep in sync with `renderDemoTasksFeatureFile()`. */
|
|
62
|
+
export function createDemoTasksFeature(): FeatureDefinition {
|
|
63
|
+
return defineFeature("tasks", (r) => {
|
|
64
|
+
r.translations({ keys: TASKS_I18N });
|
|
65
|
+
r.entity("task", taskEntity);
|
|
66
|
+
r.writeHandler(defineEntityCreateHandler("task", taskEntity, open));
|
|
67
|
+
r.writeHandler(defineEntityUpdateHandler("task", taskEntity, open));
|
|
68
|
+
r.writeHandler(defineEntityDeleteHandler("task", taskEntity, open));
|
|
69
|
+
r.queryHandler(defineEntityListHandler("task", taskEntity, open));
|
|
70
|
+
r.queryHandler(defineEntityDetailHandler("task", taskEntity, open));
|
|
71
|
+
r.screen(listScreen);
|
|
72
|
+
r.screen(editScreen);
|
|
73
|
+
r.nav({ id: "tasks", label: "Tasks", order: 10, screen: "tasks:screen:task-list" });
|
|
74
|
+
r.nav({
|
|
75
|
+
id: "task-new",
|
|
76
|
+
label: "New task",
|
|
77
|
+
parent: "tasks:nav:tasks",
|
|
78
|
+
screen: "tasks:screen:task-edit",
|
|
79
|
+
order: 10,
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function renderDemoTasksFeatureFile(): string {
|
|
85
|
+
return `// Demo tasks feature — scaffolded by \`kumiko new app\`. Edit or replace.
|
|
86
|
+
// Entity + CRUD handlers + list/edit screens + sidebar nav.
|
|
87
|
+
|
|
88
|
+
import {
|
|
89
|
+
createBooleanField,
|
|
90
|
+
createEntity,
|
|
91
|
+
createNumberField,
|
|
92
|
+
createTextField,
|
|
93
|
+
defineEntityCreateHandler,
|
|
94
|
+
defineEntityDeleteHandler,
|
|
95
|
+
defineEntityDetailHandler,
|
|
96
|
+
defineEntityListHandler,
|
|
97
|
+
defineEntityUpdateHandler,
|
|
98
|
+
defineFeature,
|
|
99
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
100
|
+
import type {
|
|
101
|
+
EntityEditScreenDefinition,
|
|
102
|
+
EntityListScreenDefinition,
|
|
103
|
+
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
104
|
+
import { tasksTranslationKeys } from "./i18n";
|
|
105
|
+
|
|
106
|
+
const taskEntity = createEntity({
|
|
107
|
+
fields: {
|
|
108
|
+
title: createTextField({ required: true, sortable: true }),
|
|
109
|
+
status: createTextField({ sortable: true }),
|
|
110
|
+
priority: createNumberField(),
|
|
111
|
+
isUrgent: createBooleanField({ default: false }),
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const listScreen: EntityListScreenDefinition = {
|
|
116
|
+
id: "task-list",
|
|
117
|
+
type: "entityList",
|
|
118
|
+
entity: "task",
|
|
119
|
+
columns: ["title", "status", "isUrgent", "priority"],
|
|
120
|
+
defaultSort: { field: "title", dir: "asc" },
|
|
121
|
+
rowActions: [{ kind: "navigate", id: "edit", label: "Edit", screen: "task-edit" }],
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const editScreen: EntityEditScreenDefinition = {
|
|
125
|
+
id: "task-edit",
|
|
126
|
+
type: "entityEdit",
|
|
127
|
+
entity: "task",
|
|
128
|
+
layout: {
|
|
129
|
+
sections: [{ title: "Task", fields: ["title", "status", "priority", "isUrgent"] }],
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const open = { access: { openToAll: true } } as const;
|
|
134
|
+
|
|
135
|
+
export const tasksFeature = defineFeature("tasks", (r) => {
|
|
136
|
+
r.translations({ keys: tasksTranslationKeys });
|
|
137
|
+
r.entity("task", taskEntity);
|
|
138
|
+
r.writeHandler(defineEntityCreateHandler("task", taskEntity, open));
|
|
139
|
+
r.writeHandler(defineEntityUpdateHandler("task", taskEntity, open));
|
|
140
|
+
r.writeHandler(defineEntityDeleteHandler("task", taskEntity, open));
|
|
141
|
+
r.queryHandler(defineEntityListHandler("task", taskEntity, open));
|
|
142
|
+
r.queryHandler(defineEntityDetailHandler("task", taskEntity, open));
|
|
143
|
+
r.screen(listScreen);
|
|
144
|
+
r.screen(editScreen);
|
|
145
|
+
r.nav({ id: "tasks", label: "Tasks", order: 10, screen: "tasks:screen:task-list" });
|
|
146
|
+
r.nav({
|
|
147
|
+
id: "task-new",
|
|
148
|
+
label: "New task",
|
|
149
|
+
parent: "tasks:nav:tasks",
|
|
150
|
+
screen: "tasks:screen:task-edit",
|
|
151
|
+
order: 10,
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function renderDemoTasksI18n(): string {
|
|
158
|
+
return `// Server-side translation keys for the demo tasks feature.
|
|
159
|
+
|
|
160
|
+
import type { TranslationsByLocale } from "@cosmicdrift/kumiko-renderer";
|
|
161
|
+
|
|
162
|
+
export const tasksTranslationKeys = {
|
|
163
|
+
"screen:task-list.title": { de: "Aufgaben", en: "Tasks" },
|
|
164
|
+
"screen:task-edit.title": { de: "Aufgabe", en: "Task" },
|
|
165
|
+
"tasks:entity:task:field:title": { de: "Titel", en: "Title" },
|
|
166
|
+
"tasks:entity:task:field:status": { de: "Status", en: "Status" },
|
|
167
|
+
"tasks:entity:task:field:priority": { de: "Priorität", en: "Priority" },
|
|
168
|
+
"tasks:entity:task:field:isUrgent": { de: "Dringend", en: "Urgent" },
|
|
169
|
+
} as const;
|
|
170
|
+
|
|
171
|
+
export const tasksTranslations: TranslationsByLocale = {
|
|
172
|
+
de: {
|
|
173
|
+
"screen:task-list.title": "Aufgaben",
|
|
174
|
+
"screen:task-edit.title": "Aufgabe",
|
|
175
|
+
"tasks:entity:task:field:title": "Titel",
|
|
176
|
+
"tasks:entity:task:field:status": "Status",
|
|
177
|
+
"tasks:entity:task:field:priority": "Priorität",
|
|
178
|
+
"tasks:entity:task:field:isUrgent": "Dringend",
|
|
179
|
+
},
|
|
180
|
+
en: {
|
|
181
|
+
"screen:task-list.title": "Tasks",
|
|
182
|
+
"screen:task-edit.title": "Task",
|
|
183
|
+
"tasks:entity:task:field:title": "Title",
|
|
184
|
+
"tasks:entity:task:field:status": "Status",
|
|
185
|
+
"tasks:entity:task:field:priority": "Priority",
|
|
186
|
+
"tasks:entity:task:field:isUrgent": "Urgent",
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function renderDemoTasksWebIndex(): string {
|
|
193
|
+
return `import type { ClientFeatureDefinition } from "@cosmicdrift/kumiko-renderer-web";
|
|
194
|
+
import { tasksTranslations } from "../i18n";
|
|
195
|
+
|
|
196
|
+
export const tasksClient: ClientFeatureDefinition = {
|
|
197
|
+
name: "tasks",
|
|
198
|
+
translations: tasksTranslations,
|
|
199
|
+
};
|
|
200
|
+
`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function renderDemoTasksIndex(): string {
|
|
204
|
+
return `export { tasksFeature } from "./feature";
|
|
205
|
+
`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function renderDemoSeedFile(): string {
|
|
209
|
+
return `// Demo seed — a few tasks so \`bun dev\` shows a non-empty list.
|
|
210
|
+
// Idempotent: skips when the tenant already has tasks (persistent dev DB).
|
|
211
|
+
|
|
212
|
+
import type { SeedFn } from "@cosmicdrift/kumiko-dev-server";
|
|
213
|
+
import { TestUsers } from "@cosmicdrift/kumiko-framework/stack";
|
|
214
|
+
|
|
215
|
+
const DEMO_TASKS = [
|
|
216
|
+
{ title: "Welcome to Kumiko", status: "todo", priority: 1, isUrgent: false },
|
|
217
|
+
{ title: "Try editing me", status: "in progress", priority: 2, isUrgent: true },
|
|
218
|
+
] as const;
|
|
219
|
+
|
|
220
|
+
export const seedDemoTasks: SeedFn = async (stack) => {
|
|
221
|
+
const admin = TestUsers.admin;
|
|
222
|
+
const existing = await stack.http.queryOk<{ rows: unknown[] }>(
|
|
223
|
+
"tasks:query:task:list",
|
|
224
|
+
{},
|
|
225
|
+
admin,
|
|
226
|
+
);
|
|
227
|
+
if (existing.rows.length > 0) return;
|
|
228
|
+
for (const task of DEMO_TASKS) {
|
|
229
|
+
await stack.http.write("tasks:write:task:create", task, admin);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
`;
|
|
233
|
+
}
|
package/src/schema-apply.ts
CHANGED
|
@@ -22,7 +22,10 @@ import {
|
|
|
22
22
|
createProjectionStateTable,
|
|
23
23
|
rebuildProjection,
|
|
24
24
|
} from "@cosmicdrift/kumiko-framework/pipeline";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
type ComposeFeaturesOptions,
|
|
27
|
+
composeFeatures,
|
|
28
|
+
} from "@cosmicdrift/kumiko-server-runtime/compose-features";
|
|
26
29
|
|
|
27
30
|
export type SchemaApplyOptions = ComposeFeaturesOptions & {
|
|
28
31
|
/** App-Features (z.B. APP_FEATURES aus run-config) — composed mit den
|
|
@@ -100,8 +103,18 @@ async function rebuildAffectedProjections(
|
|
|
100
103
|
const projections = new Set<string>();
|
|
101
104
|
for (const table of changedTables) {
|
|
102
105
|
const name = tableToProjection.get(table);
|
|
103
|
-
if (name)
|
|
106
|
+
if (name) {
|
|
107
|
+
projections.add(name);
|
|
108
|
+
} else {
|
|
109
|
+
// 522/3: a table in a .rebuild.json marker that no longer matches any
|
|
110
|
+
// registered projection would otherwise rebuild nothing and exit 0 —
|
|
111
|
+
// indistinguishable from "nothing needed a rebuild".
|
|
112
|
+
console.warn(
|
|
113
|
+
` ⚠ Table "${table}" is in a rebuild marker but matches no registered projection — skipped.`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
104
116
|
}
|
|
117
|
+
// skip: no projections matched the changed tables, nothing to rebuild
|
|
105
118
|
if (projections.size === 0) return;
|
|
106
119
|
|
|
107
120
|
console.log(` Rebuild ${projections.size} Projection(s)…`);
|
package/src/schema-check-core.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { existsSync } from "node:fs";
|
|
6
6
|
import { resolve } from "node:path";
|
|
7
|
-
import { composeFeatures } from "
|
|
7
|
+
import { composeFeatures } from "@cosmicdrift/kumiko-server-runtime/compose-features";
|
|
8
8
|
|
|
9
9
|
// The generate-script lives under different roots across apps: publicstatus
|
|
10
10
|
// uses `drizzle/generate.ts`, the framework's own sample-apps use
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Test helper: composeFeatures + setupTestStack in one call.
|
|
2
|
+
// Apps pass the same feature list as run-config (APP_FEATURES / buildAppFeatures).
|
|
3
|
+
|
|
4
|
+
import { createConfigResolver } from "@cosmicdrift/kumiko-bundled-features/config";
|
|
5
|
+
import { createTextContentApi } from "@cosmicdrift/kumiko-bundled-features/text-content";
|
|
6
|
+
import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
7
|
+
import {
|
|
8
|
+
setupTestStack,
|
|
9
|
+
type TestStack,
|
|
10
|
+
type TestStackOptions,
|
|
11
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
12
|
+
import {
|
|
13
|
+
type ComposeFeaturesOptions,
|
|
14
|
+
composeFeatures,
|
|
15
|
+
} from "@cosmicdrift/kumiko-server-runtime/compose-features";
|
|
16
|
+
|
|
17
|
+
export type TestStackPreset = "config" | "text-content";
|
|
18
|
+
|
|
19
|
+
export type SetupTestStackFromFeaturesOptions = Omit<TestStackOptions, "features"> & {
|
|
20
|
+
readonly includeBundled?: boolean;
|
|
21
|
+
readonly authOptions?: ComposeFeaturesOptions["authOptions"];
|
|
22
|
+
readonly presets?: readonly TestStackPreset[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function mergeExtraContext(
|
|
26
|
+
base: TestStackOptions["extraContext"],
|
|
27
|
+
presets: readonly TestStackPreset[],
|
|
28
|
+
): TestStackOptions["extraContext"] {
|
|
29
|
+
if (presets.length === 0) return base;
|
|
30
|
+
|
|
31
|
+
return (deps) => {
|
|
32
|
+
const fromBase =
|
|
33
|
+
typeof base === "function" ? base(deps) : base !== undefined ? { ...base } : {};
|
|
34
|
+
const merged: Record<string, unknown> = { ...fromBase };
|
|
35
|
+
|
|
36
|
+
if (presets.includes("config")) {
|
|
37
|
+
const configResolver = createConfigResolver();
|
|
38
|
+
merged["configResolver"] = configResolver;
|
|
39
|
+
}
|
|
40
|
+
if (presets.includes("text-content")) {
|
|
41
|
+
merged["textContent"] = createTextContentApi(deps.db);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return merged;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function setupTestStackFromFeatures(
|
|
49
|
+
appFeatures: readonly FeatureDefinition[],
|
|
50
|
+
options: SetupTestStackFromFeaturesOptions = {},
|
|
51
|
+
): Promise<TestStack> {
|
|
52
|
+
const { includeBundled = false, authOptions, presets = [], ...stackOptions } = options;
|
|
53
|
+
const features = composeFeatures(appFeatures, { includeBundled, authOptions });
|
|
54
|
+
const extraContext = mergeExtraContext(stackOptions.extraContext, presets);
|
|
55
|
+
|
|
56
|
+
return setupTestStack({
|
|
57
|
+
...stackOptions,
|
|
58
|
+
features,
|
|
59
|
+
...(extraContext !== undefined && { extraContext }),
|
|
60
|
+
});
|
|
61
|
+
}
|
package/src/welcome-banner.ts
CHANGED
|
@@ -37,10 +37,7 @@ export function renderWelcomeBanner(input: WelcomeBannerInput): string {
|
|
|
37
37
|
return [top, ...padded, bottom].join("\n");
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
//
|
|
41
|
-
// enough for ASCII + the small set of arrows/checkmarks used above; if a
|
|
42
|
-
// real wide-char ever lands in the banner the row alignment will drift,
|
|
43
|
-
// caught visually by the snapshot test.
|
|
40
|
+
// ponytail: codepoint width; ok for ASCII + arrows, add a width-lib if CJK ever lands here.
|
|
44
41
|
function stringWidth(s: string): number {
|
|
45
42
|
return [...s].length;
|
|
46
43
|
}
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { createDeliveryFeature } from "@cosmicdrift/kumiko-bundled-features/delivery";
|
|
3
|
-
import { createSecretsFeature } from "@cosmicdrift/kumiko-bundled-features/secrets";
|
|
4
|
-
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
5
|
-
import { createRegistry, defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
6
|
-
import type { MasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
|
|
7
|
-
import { buildBootExtraContext } from "../run-prod-app";
|
|
8
|
-
|
|
9
|
-
// Pins runProdApp's framework-default-provider autowire (buildBootExtraContext):
|
|
10
|
-
// textContent unconditional, secrets feature-gated, KEK-env trap avoided, the
|
|
11
|
-
// money-horse regression (no secrets feature → no forced KEK → no throw), and
|
|
12
|
-
// the delivery _notifyFactory wiring that makes ctx.notify work in prod (only
|
|
13
|
-
// test-wired before — runProdApp/runDevApp call THIS, not createDeliveryTestContext).
|
|
14
|
-
|
|
15
|
-
// db is never touched at construction time — createTextContentApi /
|
|
16
|
-
// createSecretsContext only store the handle and query lazily. A bare stub
|
|
17
|
-
// keeps this a fast unit test (no Postgres) instead of a full boot.
|
|
18
|
-
const fakeDb = {} as unknown as DbConnection;
|
|
19
|
-
const registry = createRegistry([]);
|
|
20
|
-
const KEK = Buffer.alloc(32, 7).toString("base64");
|
|
21
|
-
|
|
22
|
-
const otherFeature = defineFeature("widgets", () => {});
|
|
23
|
-
|
|
24
|
-
describe("buildBootExtraContext — framework-default provider autowire", () => {
|
|
25
|
-
test("textContent is wired unconditionally (no secrets feature, no auth)", () => {
|
|
26
|
-
const ctx = buildBootExtraContext({
|
|
27
|
-
db: fakeDb,
|
|
28
|
-
features: [otherFeature],
|
|
29
|
-
envSource: {},
|
|
30
|
-
registry,
|
|
31
|
-
hasAuth: false,
|
|
32
|
-
});
|
|
33
|
-
expect(typeof (ctx["textContent"] as { getBlock?: unknown }).getBlock).toBe("function");
|
|
34
|
-
expect(ctx["secrets"]).toBeUndefined();
|
|
35
|
-
expect(ctx["configResolver"]).toBeUndefined();
|
|
36
|
-
expect(ctx["_notifyFactory"]).toBeUndefined();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test("delivery feature mounted → _notifyFactory wired (ctx.notify works in prod)", () => {
|
|
40
|
-
const ctx = buildBootExtraContext({
|
|
41
|
-
db: fakeDb,
|
|
42
|
-
features: [createDeliveryFeature()],
|
|
43
|
-
envSource: {},
|
|
44
|
-
registry,
|
|
45
|
-
hasAuth: false,
|
|
46
|
-
});
|
|
47
|
-
expect(typeof ctx["_notifyFactory"]).toBe("function");
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test("no delivery feature → _notifyFactory not wired", () => {
|
|
51
|
-
const ctx = buildBootExtraContext({
|
|
52
|
-
db: fakeDb,
|
|
53
|
-
features: [otherFeature],
|
|
54
|
-
envSource: {},
|
|
55
|
-
registry,
|
|
56
|
-
hasAuth: false,
|
|
57
|
-
});
|
|
58
|
-
expect(ctx["_notifyFactory"]).toBeUndefined();
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("secrets feature mounted + valid KEK env → ctx.secrets wired", () => {
|
|
62
|
-
const ctx = buildBootExtraContext({
|
|
63
|
-
db: fakeDb,
|
|
64
|
-
features: [createSecretsFeature()],
|
|
65
|
-
envSource: {
|
|
66
|
-
KUMIKO_SECRETS_MASTER_KEY_V1: KEK,
|
|
67
|
-
KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION: "1",
|
|
68
|
-
},
|
|
69
|
-
registry,
|
|
70
|
-
hasAuth: false,
|
|
71
|
-
});
|
|
72
|
-
const secrets = ctx["secrets"] as {
|
|
73
|
-
get?: unknown;
|
|
74
|
-
set?: unknown;
|
|
75
|
-
has?: unknown;
|
|
76
|
-
delete?: unknown;
|
|
77
|
-
};
|
|
78
|
-
expect(typeof secrets.get).toBe("function");
|
|
79
|
-
expect(typeof secrets.set).toBe("function");
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test("secrets feature mounted, only V1 set (no CURRENT_VERSION) → wired (default '1')", () => {
|
|
83
|
-
const ctx = buildBootExtraContext({
|
|
84
|
-
db: fakeDb,
|
|
85
|
-
features: [createSecretsFeature()],
|
|
86
|
-
envSource: { KUMIKO_SECRETS_MASTER_KEY_V1: KEK },
|
|
87
|
-
registry,
|
|
88
|
-
hasAuth: false,
|
|
89
|
-
});
|
|
90
|
-
expect(ctx["secrets"]).toBeDefined();
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test("secrets feature mounted but NO KEK + no override → skipped, no throw (dev DEV_KEY path)", () => {
|
|
94
|
-
// dev wires its own DEV_KEY secrets explicitly; the auto-wire must not
|
|
95
|
-
// eagerly construct an env-provider and crash the boot when no env KEK
|
|
96
|
-
// exists. ctx.secrets stays unset so the app's explicit wiring wins.
|
|
97
|
-
let ctx: Record<string, unknown> | undefined;
|
|
98
|
-
expect(() => {
|
|
99
|
-
ctx = buildBootExtraContext({
|
|
100
|
-
db: fakeDb,
|
|
101
|
-
features: [createSecretsFeature()],
|
|
102
|
-
envSource: {},
|
|
103
|
-
registry,
|
|
104
|
-
hasAuth: false,
|
|
105
|
-
});
|
|
106
|
-
}).not.toThrow();
|
|
107
|
-
expect(ctx?.["secrets"]).toBeUndefined();
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
test("no secrets feature → no KEK ever read, boots green (money-horse regression)", () => {
|
|
111
|
-
const ctx = buildBootExtraContext({
|
|
112
|
-
db: fakeDb,
|
|
113
|
-
features: [otherFeature],
|
|
114
|
-
envSource: {}, // no KEK — must NOT matter when secrets isn't mounted
|
|
115
|
-
registry,
|
|
116
|
-
hasAuth: false,
|
|
117
|
-
});
|
|
118
|
-
expect(ctx["secrets"]).toBeUndefined();
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test("masterKey override is used verbatim — no env-provider built", () => {
|
|
122
|
-
const override: MasterKeyProvider = {
|
|
123
|
-
wrapDek: async () => ({ encryptedDek: Buffer.alloc(0), kekVersion: 1 }),
|
|
124
|
-
unwrapDek: async () => Buffer.alloc(0),
|
|
125
|
-
currentVersion: () => 1,
|
|
126
|
-
isAvailable: async () => true,
|
|
127
|
-
};
|
|
128
|
-
// envSource is empty: if the override weren't honoured, createEnvMasterKeyProvider
|
|
129
|
-
// would throw "no KEK". It doesn't → the override path is taken.
|
|
130
|
-
const ctx = buildBootExtraContext({
|
|
131
|
-
db: fakeDb,
|
|
132
|
-
features: [createSecretsFeature()],
|
|
133
|
-
envSource: {},
|
|
134
|
-
registry,
|
|
135
|
-
hasAuth: false,
|
|
136
|
-
masterKey: override,
|
|
137
|
-
});
|
|
138
|
-
expect(ctx["secrets"]).toBeDefined();
|
|
139
|
-
});
|
|
140
|
-
});
|