@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.
Files changed (66) hide show
  1. package/bin/kumiko-schema-check.ts +7 -0
  2. package/package.json +12 -7
  3. package/src/__tests__/build-prod-bundle.integration.test.ts +1 -1
  4. package/src/__tests__/build-server-bundle.test.ts +62 -0
  5. package/src/__tests__/compose-stacks.test.ts +271 -0
  6. package/src/__tests__/create-kumiko-server-errors.test.ts +54 -0
  7. package/src/__tests__/create-kumiko-server-options.test.ts +33 -0
  8. package/src/__tests__/create-kumiko-server.integration.test.ts +128 -1
  9. package/src/__tests__/discover-format.test.ts +1 -1
  10. package/src/__tests__/env-schema.integration.test.ts +1 -1
  11. package/src/__tests__/few-shot-corpus.test.ts +16 -11
  12. package/src/__tests__/merge-extra-context.test.ts +56 -0
  13. package/src/__tests__/resolve-stylesheet.test.ts +16 -0
  14. package/src/__tests__/saas-identity-wire.integration.test.ts +430 -0
  15. package/src/__tests__/scaffold-app-feature.test.ts +59 -6
  16. package/src/__tests__/scaffold-app.test.ts +34 -2
  17. package/src/__tests__/schema-apply.integration.test.ts +5 -2
  18. package/src/__tests__/setup-test-stack-from-features.integration.test.ts +30 -0
  19. package/src/__tests__/walkthrough.integration.test.ts +22 -6
  20. package/src/build-server-bundle.ts +33 -15
  21. package/src/build.ts +1 -2
  22. package/src/codegen/__tests__/render-codegen.test.ts +2 -1
  23. package/src/codegen/__tests__/run-codegen.test.ts +2 -2
  24. package/src/codegen/__tests__/strict-mode-diagnostics.test.ts +6 -1
  25. package/src/codegen/__tests__/watch.test.ts +1 -2
  26. package/src/codegen/render.ts +6 -1
  27. package/src/codegen/scan-events.ts +7 -5
  28. package/src/codegen/watch.ts +7 -4
  29. package/src/compose-stacks.ts +184 -0
  30. package/src/create-kumiko-server.ts +28 -5
  31. package/src/few-shot-corpus.ts +4 -3
  32. package/src/index.ts +30 -12
  33. package/src/run-dev-app.ts +195 -61
  34. package/src/scaffold-app-feature.ts +118 -15
  35. package/src/scaffold-app.ts +151 -79
  36. package/src/scaffold-demo-tasks.ts +233 -0
  37. package/src/schema-apply.ts +15 -2
  38. package/src/schema-check-core.ts +1 -1
  39. package/src/setup-test-stack-from-features.ts +61 -0
  40. package/src/welcome-banner.ts +1 -4
  41. package/src/__tests__/boot-extra-context.test.ts +0 -140
  42. package/src/__tests__/build-prod-bundle.test.ts +0 -311
  43. package/src/__tests__/cache-headers.test.ts +0 -83
  44. package/src/__tests__/compose-features-wiring.integration.test.ts +0 -382
  45. package/src/__tests__/compose-features.test.ts +0 -129
  46. package/src/__tests__/config-seed-boot.integration.test.ts +0 -158
  47. package/src/__tests__/inject-schema.test.ts +0 -62
  48. package/src/__tests__/renderer-web-css-relocation.integration.test.ts +0 -85
  49. package/src/__tests__/renderer-web-shell-sentinel.test.ts +0 -35
  50. package/src/__tests__/require-env.test.ts +0 -29
  51. package/src/__tests__/resolve-auth-mail.test.ts +0 -69
  52. package/src/__tests__/resolve-tailwind-cli.test.ts +0 -81
  53. package/src/__tests__/run-prod-app-env-source.test.ts +0 -157
  54. package/src/__tests__/run-prod-app-spec.test.ts +0 -57
  55. package/src/__tests__/run-prod-app.integration.test.ts +0 -840
  56. package/src/__tests__/session-wiring.test.ts +0 -51
  57. package/src/__tests__/try-hono-first.test.ts +0 -63
  58. package/src/boot/apply-boot-seeds.ts +0 -18
  59. package/src/build-prod-bundle.ts +0 -697
  60. package/src/compose-features.ts +0 -145
  61. package/src/extra-routes-deps.ts +0 -47
  62. package/src/inject-schema.ts +0 -24
  63. package/src/resolve-tailwind-cli.ts +0 -45
  64. package/src/run-prod-app.ts +0 -1492
  65. package/src/session-wiring.ts +0 -29
  66. 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
+ }
@@ -22,7 +22,10 @@ import {
22
22
  createProjectionStateTable,
23
23
  rebuildProjection,
24
24
  } from "@cosmicdrift/kumiko-framework/pipeline";
25
- import { type ComposeFeaturesOptions, composeFeatures } from "./compose-features";
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) projections.add(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)…`);
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { existsSync } from "node:fs";
6
6
  import { resolve } from "node:path";
7
- import { composeFeatures } from "./compose-features";
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
+ }
@@ -37,10 +37,7 @@ export function renderWelcomeBanner(input: WelcomeBannerInput): string {
37
37
  return [top, ...padded, bottom].join("\n");
38
38
  }
39
39
 
40
- // Plain monospace-cell width counts each codepoint as one cell. Good
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
- });