@cosmicdrift/kumiko-dev-server 0.24.0 → 0.24.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-dev-server",
3
- "version": "0.24.0",
3
+ "version": "0.24.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>",
@@ -0,0 +1,104 @@
1
+ // Regression: the boot-path must read the injected `envSource`, not the real
2
+ // process.env. Boot-mode (KUMIKO_DRY_RUN_ENV=boot) validates wiring + builds
3
+ // the registry, then tears down the lazy DB/Redis clients before any socket
4
+ // opens — so this runs without a real Postgres/Redis (same as the CI boot
5
+ // smoke). Before the fix, requireEnv/readEnv read process.env directly, so the
6
+ // required-var test would throw "required env var DATABASE_URL is missing" and
7
+ // the PORT test would bind the default instead of the injected port.
8
+
9
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
10
+ import {
11
+ createBooleanField,
12
+ createEntity,
13
+ createTextField,
14
+ defineFeature,
15
+ } from "@cosmicdrift/kumiko-framework/engine";
16
+ import { z } from "zod";
17
+ import { runProdApp } from "../run-prod-app";
18
+
19
+ const probeEntity = createEntity({
20
+ fields: {
21
+ name: createTextField({ required: true }),
22
+ active: createBooleanField({ default: true }),
23
+ },
24
+ table: "env_source_probe",
25
+ });
26
+
27
+ const probeFeature = defineFeature("env-source-probe", (r) => {
28
+ r.entity("widget", probeEntity);
29
+ r.queryHandler({
30
+ name: "ping",
31
+ schema: z.object({}),
32
+ access: { roles: ["anonymous"] },
33
+ handler: async () => ({ pong: true }),
34
+ });
35
+ });
36
+
37
+ // Cleared from process.env so the test fully controls config via envSource.
38
+ // DATABASE_URL/REDIS_URL/JWT_SECRET are required (their read throws pre-fix);
39
+ // PORT is non-throwing, cleared only so ambient PORT can't mask the second
40
+ // test's "PORT comes from envSource" assertion.
41
+ const CLEARED_VARS = ["DATABASE_URL", "REDIS_URL", "JWT_SECRET", "PORT"] as const;
42
+
43
+ const DUMMY_ENV = {
44
+ KUMIKO_DRY_RUN_ENV: "boot",
45
+ DATABASE_URL: "postgres://smoke:smoke@127.0.0.1:1/smoke",
46
+ REDIS_URL: "redis://127.0.0.1:1",
47
+ JWT_SECRET: "smokesmokesmokesmokesmokesmokesmokesmoke",
48
+ } as const;
49
+
50
+ describe("runProdApp boot-mode env-source", () => {
51
+ const saved: Record<string, string | undefined> = {};
52
+
53
+ beforeEach(() => {
54
+ for (const k of CLEARED_VARS) {
55
+ saved[k] = process.env[k];
56
+ delete process.env[k];
57
+ }
58
+ });
59
+
60
+ afterEach(() => {
61
+ for (const k of CLEARED_VARS) {
62
+ if (saved[k] === undefined) delete process.env[k];
63
+ else process.env[k] = saved[k];
64
+ }
65
+ });
66
+
67
+ test("boots from injected envSource even when process.env lacks the required vars", async () => {
68
+ const handle = await runProdApp({
69
+ features: [probeFeature],
70
+ autoListen: false,
71
+ migrations: false,
72
+ envSource: { ...DUMMY_ENV },
73
+ });
74
+
75
+ // Boot-mode with an injected envSource returns an inert dry-run handle.
76
+ expect(handle).toBeDefined();
77
+ expect(typeof handle.stop).toBe("function");
78
+ await handle.stop();
79
+ });
80
+
81
+ test("resolves PORT from envSource, not process.env", async () => {
82
+ const logs: string[] = [];
83
+ const originalLog = console.log;
84
+ console.log = (...args: unknown[]) => {
85
+ logs.push(args.map(String).join(" "));
86
+ };
87
+ try {
88
+ const handle = await runProdApp({
89
+ features: [probeFeature],
90
+ autoListen: false,
91
+ migrations: false,
92
+ envSource: { ...DUMMY_ENV, PORT: "8123" },
93
+ });
94
+ await handle.stop();
95
+ } finally {
96
+ console.log = originalLog;
97
+ }
98
+
99
+ // The boot logs "booting Kumiko stack on port <port>" — pre-fix this read
100
+ // process.env["PORT"] (deleted here) and would log the 3000 default.
101
+ expect(logs.some((line) => line.includes("port 8123"))).toBe(true);
102
+ expect(logs.some((line) => line.includes("port 3000"))).toBe(false);
103
+ });
104
+ });
@@ -1,4 +1,4 @@
1
- import { afterEach, beforeEach, describe, expect, it } from "bun:test";
1
+ import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test";
2
2
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
@@ -205,10 +205,17 @@ describe("scaffoldDeploy", () => {
205
205
  expect(df).not.toContain("ENV GITHUB_TOKEN");
206
206
  });
207
207
 
208
- it("malformed package.json doesn't crash detection (defaults to no private deps)", () => {
209
- writeFileSync(join(tmp, "package.json"), "{ this is not json");
210
- const result = scaffoldDeploy({ appName: "broken", destination: tmp });
211
- expect(result.detected.hasPrivateGhPackages).toBe(false);
208
+ it("malformed package.json warns + defaults to no private deps (mis-detection is visible)", () => {
209
+ const warn = spyOn(console, "warn").mockImplementation(() => {});
210
+ try {
211
+ writeFileSync(join(tmp, "package.json"), "{ this is not json");
212
+ const result = scaffoldDeploy({ appName: "broken", destination: tmp });
213
+ expect(result.detected.hasPrivateGhPackages).toBe(false);
214
+ expect(warn).toHaveBeenCalledTimes(1);
215
+ expect(warn.mock.calls[0]?.[0]).toContain("is not valid JSON");
216
+ } finally {
217
+ warn.mockRestore();
218
+ }
212
219
  });
213
220
  });
214
221
  });
package/src/env-schema.ts CHANGED
@@ -47,7 +47,7 @@ export const frameworkCoreEnvSchema = z.object({
47
47
  ),
48
48
 
49
49
  // `z.string().optional()` (not `z.literal("1")`) — the run-prod-app
50
- // call-site (`process.env["KUMIKO_SKIP_ES_OPS"] !== "1"`) ignores any
50
+ // call-site (`envSource["KUMIKO_SKIP_ES_OPS"] !== "1"`) ignores any
51
51
  // value other than literal "1". A stricter schema would reject e.g.
52
52
  // "true" / "yes" that the runtime silently ignores, surfacing
53
53
  // boot-errors for inputs the framework doesn't actually care about.
@@ -110,8 +110,11 @@ export function buildBunServeOptions(
110
110
 
111
111
  // Strict env-var read. Throws with a clear hint when missing — better
112
112
  // than discovering a Postgres-connection-refused 30s into the boot.
113
- function requireEnv(name: string): string {
114
- const value = process.env[name];
113
+ // `src` defaults to process.env but is threaded from the caller's envSource
114
+ // so the boot-path reads the SAME env-quelle that was validated above —
115
+ // injected dummies in test-mode must not silently fall back to process.env.
116
+ function requireEnv(name: string, src: Record<string, string | undefined> = process.env): string {
117
+ const value = src[name];
115
118
  if (value === undefined || value === "") {
116
119
  throw new Error(
117
120
  `runProdApp: required env var "${name}" is missing or empty. ` +
@@ -123,8 +126,11 @@ function requireEnv(name: string): string {
123
126
 
124
127
  // Optional env helper — returns undefined for missing, string for set.
125
128
  // Used for KUMIKO_INSTANCE_ID, JWT_ISSUER and other "nice to have" knobs.
126
- function readEnv(name: string): string | undefined {
127
- const value = process.env[name];
129
+ function readEnv(
130
+ name: string,
131
+ src: Record<string, string | undefined> = process.env,
132
+ ): string | undefined {
133
+ const value = src[name];
128
134
  return value === undefined || value === "" ? undefined : value;
129
135
  }
130
136
 
@@ -535,12 +541,12 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
535
541
  // 2. Env-vars: fail-fast. Better a 0s boot crash with a clear error
536
542
  // than a 30s timeout chasing a Postgres connection that was never
537
543
  // configured.
538
- const databaseUrl = requireEnv("DATABASE_URL");
539
- const redisUrl = requireEnv("REDIS_URL");
540
- const jwtSecret = requireEnv("JWT_SECRET");
541
- const jwtIssuer = readEnv("JWT_ISSUER");
542
- const instanceId = readEnv("KUMIKO_INSTANCE_ID");
543
- const port = options.port ?? Number.parseInt(process.env["PORT"] ?? "3000", 10);
544
+ const databaseUrl = requireEnv("DATABASE_URL", envSource);
545
+ const redisUrl = requireEnv("REDIS_URL", envSource);
546
+ const jwtSecret = requireEnv("JWT_SECRET", envSource);
547
+ const jwtIssuer = readEnv("JWT_ISSUER", envSource);
548
+ const instanceId = readEnv("KUMIKO_INSTANCE_ID", envSource);
549
+ const port = options.port ?? Number.parseInt(envSource["PORT"] ?? "3000", 10);
544
550
 
545
551
  // biome-ignore lint/suspicious/noConsole: boot-time progress hint, no logger configured this early
546
552
  console.log(`[runProdApp] booting Kumiko stack on port ${port}…`);
@@ -774,7 +780,7 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
774
780
  // if-missing"-Schicht; seed-migrations sind die "diff-and-update"-
775
781
  // Schicht für Drift den existing Seeds nicht erfassen können (z.B.
776
782
  // Membership-Roles-Change nach initialer Seed-Erstellung).
777
- if (options.seedsDir !== undefined && process.env["KUMIKO_SKIP_ES_OPS"] !== "1") {
783
+ if (options.seedsDir !== undefined && envSource["KUMIKO_SKIP_ES_OPS"] !== "1") {
778
784
  await createEsOperationsTable(db);
779
785
  const seedDispatcher = createDispatcher(registry, {
780
786
  db,
@@ -249,7 +249,7 @@ function renderMain(appName: string): string {
249
249
  "// Production-bootstrap. KUMIKO_DRY_RUN_ENV=boot exits after",
250
250
  "// composeFeatures + validateBoot + createRegistry without DB/Redis-connect",
251
251
  "// (siehe @cosmicdrift/kumiko-dev-server runProdApp). Echter Dev-Boot",
252
- "// passiert via `bun kumiko dev` (in-repo dev-tool) mit Docker-stack — DX-1.0 deckt nur",
252
+ "// passiert via `bunx kumiko dev` (in-repo dev-tool) mit Docker-stack — DX-1.0 deckt nur",
253
253
  "// den boot-mode-Pfad ab; `kumiko dev` kommt in einer späteren DX-Phase.",
254
254
  "",
255
255
  "",
@@ -143,8 +143,14 @@ function detectOptionalSurfaces(sourceDir: string): ScaffoldDeployDetected {
143
143
  hasPrivateGhPackages = Object.keys(allDeps).some((d) =>
144
144
  d.startsWith("@cosmicdriftgamestudio/"),
145
145
  );
146
- } catch {
147
- // malformed package.json — assume no private packages, app-author can override via Dockerfile
146
+ } catch (err) {
147
+ // malformed package.json — assume no private packages, app-author can
148
+ // override via Dockerfile. Warn so a silent mis-detection (later YN0041
149
+ // on yarn install) is traceable to the scaffold step.
150
+ // biome-ignore lint/suspicious/noConsole: scaffold visibility for skipped private-package detection
151
+ console.warn(
152
+ `scaffoldDeploy: package.json at ${pkgJsonPath} is not valid JSON — private-GH-packages detection skipped (${err instanceof Error ? err.message : String(err)})`,
153
+ );
148
154
  }
149
155
  }
150
156
  return { hasSeeds, hasPrivateGhPackages };