@cosmicdrift/kumiko-dev-server 0.220.1 → 0.221.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-dev-server",
3
- "version": "0.220.1",
3
+ "version": "0.221.0",
4
4
  "description": "Dev-tooling for Kumiko apps: local dev-server bootstrap (runDevApp), scaffolding, codegen. Its compose-stacks/env-schema subpaths are consumed at prod boot too — see @cosmicdrift/kumiko-server-runtime for the prod runner itself.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -59,9 +59,9 @@
59
59
  "kumiko-upgrade": "./bin/kumiko-upgrade.ts"
60
60
  },
61
61
  "dependencies": {
62
- "@cosmicdrift/kumiko-bundled-features": "0.220.1",
63
- "@cosmicdrift/kumiko-framework": "0.220.1",
64
- "@cosmicdrift/kumiko-server-runtime": "0.220.1",
62
+ "@cosmicdrift/kumiko-bundled-features": "0.221.0",
63
+ "@cosmicdrift/kumiko-framework": "0.221.0",
64
+ "@cosmicdrift/kumiko-server-runtime": "0.221.0",
65
65
  "ts-morph": "^28.0.0"
66
66
  },
67
67
  "publishConfig": {
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, test } from "bun:test";
2
2
  import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
+ import { NO_ROUTE_MATCH_HEADER_NAME } from "@cosmicdrift/kumiko-framework/api";
5
6
  import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
6
7
  import {
7
8
  createBooleanField,
@@ -507,3 +508,66 @@ describe("createKumikoServer — hot-reload broadcast", () => {
507
508
  }
508
509
  });
509
510
  });
511
+
512
+ // kumiko-framework#2435: tryHonoFirst used to decide "no route matched"
513
+ // purely from the status code — a matched httpRoute answering 404 on
514
+ // purpose (e.g. default-deny reads) was indistinguishable from an
515
+ // unregistered path and got silently rewritten to the SPA shell with
516
+ // status 200. Drives the REAL fetch-handler end to end (real Postgres,
517
+ // real Hono app via createKumikoServer) so the fix's actual wiring
518
+ // (buildServer's app.notFound() marker + tryHonoFirst reading it) is
519
+ // under test, not just the pure-function pin in try-hono-first.test.ts.
520
+ const probeHttpRouteFeature = defineFeature("dev-server-probe-http-route", (r) => {
521
+ r.httpRoute({
522
+ method: "GET",
523
+ path: "/probe/:id",
524
+ anonymous: true,
525
+ handler: async (c) => {
526
+ const id = c.req.param("id");
527
+ if (id === "missing") return c.text("not found", 404);
528
+ return c.text(`probe:${id}`, 200);
529
+ },
530
+ });
531
+ });
532
+
533
+ describe("createKumikoServer — tryHonoFirst 404-vs-router-miss (#2435)", () => {
534
+ async function bootWithProbeRoute(): Promise<KumikoServerHandle> {
535
+ handle = await createKumikoServer({
536
+ features: [probeHttpRouteFeature],
537
+ port: 0,
538
+ installSignalHandlers: false,
539
+ });
540
+ return handle;
541
+ }
542
+
543
+ test("gematchte httpRoute mit bewusster 404 bleibt 404 (keine SPA-Maskierung)", async () => {
544
+ const h = await bootWithProbeRoute();
545
+ const res = await h.fetch(new Request("http://localhost/probe/missing"));
546
+ expect(res.status).toBe(404);
547
+ expect(res.headers.get("content-type") ?? "").not.toMatch(/text\/html/);
548
+ expect(await res.text()).toBe("not found");
549
+ });
550
+
551
+ test("gematchte httpRoute mit Treffer antwortet normal (200)", async () => {
552
+ const h = await bootWithProbeRoute();
553
+ const res = await h.fetch(new Request("http://localhost/probe/exists"));
554
+ expect(res.status).toBe(200);
555
+ expect(await res.text()).toBe("probe:exists");
556
+ });
557
+
558
+ test("unbekannte SPA-Route liefert weiterhin die SPA-Shell (200 HTML)", async () => {
559
+ const h = await bootWithProbeRoute();
560
+ const res = await h.fetch(new Request("http://localhost/some/client-side/route"));
561
+ expect(res.status).toBe(200);
562
+ expect(res.headers.get("content-type")).toMatch(/text\/html/);
563
+ expect(await res.text()).toMatch(/<div id="root">/);
564
+ });
565
+
566
+ test("kein Router-miss-Marker leakt an den Client", async () => {
567
+ const h = await bootWithProbeRoute();
568
+ const missRes = await h.fetch(new Request("http://localhost/some/client-side/route"));
569
+ const deniedRes = await h.fetch(new Request("http://localhost/probe/missing"));
570
+ expect(missRes.headers.has(NO_ROUTE_MATCH_HEADER_NAME)).toBe(false);
571
+ expect(deniedRes.headers.has(NO_ROUTE_MATCH_HEADER_NAME)).toBe(false);
572
+ });
573
+ });
@@ -42,6 +42,11 @@ describe("public-export smoke", () => {
42
42
  it("re-exports frameworkCoreEnvSchema via dev-server's package entry", () => {
43
43
  expect(devServerPublicApi.frameworkCoreEnvSchema).toBe(frameworkCoreEnvSchema);
44
44
  });
45
+
46
+ it("re-exports frameworkCoreEnvSchema via the env-schema subpath", async () => {
47
+ const viaSubpath = await import("@cosmicdrift/kumiko-dev-server/env-schema");
48
+ expect(viaSubpath.frameworkCoreEnvSchema).toBe(frameworkCoreEnvSchema);
49
+ });
45
50
  });
46
51
 
47
52
  describe("runProdApp envSchema integration", () => {
@@ -1,7 +1,6 @@
1
1
  import { describe, expect, it } from "bun:test";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { fileURLToPath } from "node:url";
4
- import { frameworkCoreEnvSchema as envSchemaViaRelative } from "../env-schema";
5
4
 
6
5
  // Mirrors infra/guards/runtime-isolation-classify.ts `classifyByDirective`
7
6
  // as of kumiko-framework#2337 (first 600 bytes, first 8 lines,
@@ -25,17 +24,3 @@ describe("compose-stacks.ts runtime directive", () => {
25
24
  expect(classifyByDirective(composeStacksPath)).toBe("runtime");
26
25
  });
27
26
  });
28
-
29
- describe("@cosmicdrift/kumiko-dev-server/env-schema subpath", () => {
30
- it("resolves the same schema as the relative import, and it parses", async () => {
31
- const { frameworkCoreEnvSchema: envSchemaViaSubpath } = await import(
32
- "@cosmicdrift/kumiko-dev-server/env-schema"
33
- );
34
- expect(envSchemaViaSubpath).toBe(envSchemaViaRelative);
35
- const parsed = envSchemaViaSubpath.parse({
36
- DATABASE_URL: "postgres://localhost:5432/db",
37
- REDIS_URL: "redis://localhost:6379",
38
- });
39
- expect(parsed.PORT).toBe("3000");
40
- });
41
- });
@@ -276,8 +276,10 @@ describe("scaffoldApp", () => {
276
276
 
277
277
  const main = readFileSync(join(dest, "bin/main.ts"), "utf-8");
278
278
  expect(main).toContain('from "@cosmicdrift/kumiko-framework/crypto"');
279
+ expect(main).toContain("requireKmsWiring(process.env");
279
280
  expect(main).toContain("resolveKmsWiring(process.env");
280
- expect(main).toContain('if ("allowPlaintextPii" in kmsWiring)');
281
+ expect(main).toContain('process.env["NODE_ENV"] === "production"');
282
+ expect(main).not.toContain('if ("allowPlaintextPii" in kmsWiring)');
281
283
  expect(main).toContain("...kmsWiring,");
282
284
  });
283
285
 
@@ -41,7 +41,10 @@ import {
41
41
  canResolveTailwindStylesheet,
42
42
  resolveTailwindCli,
43
43
  } from "@cosmicdrift/kumiko-server-runtime/resolve-tailwind-cli";
44
- import { tryHonoFirst } from "@cosmicdrift/kumiko-server-runtime/try-hono-first";
44
+ import {
45
+ stripNoRouteMatchHeader,
46
+ tryHonoFirst,
47
+ } from "@cosmicdrift/kumiko-server-runtime/try-hono-first";
45
48
 
46
49
  // Runtime-detection. The dev-server is meant to run under Bun (Kumiko's
47
50
  // target runtime), but the test-suite runs under vitest on Node — we
@@ -941,7 +944,11 @@ export async function createKumikoServer(
941
944
  return htmlResponse("client", true);
942
945
  }
943
946
 
944
- return stack.app.fetch(req);
947
+ // Bypasses tryHonoFirst entirely (API paths, dotted paths, /sse,
948
+ // non-GET/HEAD), so the router-miss marker must be stripped here too —
949
+ // otherwise an unmatched path would leak it straight to the client
950
+ // (see try-hono-first.ts's header-hygiene note).
951
+ return stripNoRouteMatchHeader(await stack.app.fetch(req));
945
952
  };
946
953
 
947
954
  // --- HTTP server (Bun only) ---
package/src/env-schema.ts CHANGED
@@ -1,3 +1,7 @@
1
+ // @runtime runtime
2
+ // Consumed at prod boot via composeEnvSchema (same as compose-stacks),
3
+ // not only by tooling — keep runtime-isolation-safe (kumiko-framework#2340).
4
+ //
1
5
  // Framework-core env-schema — the vars that runProdApp + buildServer
2
6
  // + the connection-pool read directly from process.env today. Apps merge
3
7
  // this into their app-wide schema via `composeEnvSchema({ core, ... })`.
@@ -439,7 +439,7 @@ function renderMain(appName: string): string {
439
439
  });
440
440
  sf.addImportDeclaration({
441
441
  moduleSpecifier: "@cosmicdrift/kumiko-framework/crypto",
442
- namedImports: ["resolveKmsWiring"],
442
+ namedImports: ["requireKmsWiring", "resolveKmsWiring"],
443
443
  });
444
444
  sf.addImportDeclaration({
445
445
  moduleSpecifier: "../src/run-config",
@@ -482,34 +482,40 @@ function renderMain(appName: string): string {
482
482
  });
483
483
 
484
484
  // Subject-key KMS for the pii-annotated entities the --yes recommended set
485
- // mounts (user, tenant-invitation, fileRef). Without the PLATFORM_KEK /
486
- // SUBJECT_KEYS_DATABASE_URL / KUMIKO_BLIND_INDEX_KEY trio this falls back
487
- // to plaintext PII with a loud boot warning fine to get running locally,
488
- // never for a real deploy (kumiko-framework#2330).
485
+ // mounts (user, tenant-invitation, fileRef). Local/dev may omit the trio
486
+ // (plaintext + loud warning); NODE_ENV=production uses requireKmsWiring so
487
+ // a real deploy cannot silently store plaintext PII (fw#2317 / #2339).
488
+ // assertPiiBootInvariants already warns on the plaintext path — no duplicate console.warn.
489
489
  sf.addVariableStatement({
490
490
  declarationKind: VariableDeclarationKind.Const,
491
491
  declarations: [
492
492
  {
493
- name: "kmsWiring",
493
+ name: "kmsOpts",
494
494
  initializer: (writer) => {
495
- writer.write("resolveKmsWiring(process.env, ").inlineBlock(() => {
496
- writer.writeLine(`logPrefix: "[${appName}]",`);
497
- writer.writeLine(
498
- `plaintextReason: "no PLATFORM_KEK / SUBJECT_KEYS_DATABASE_URL / KUMIKO_BLIND_INDEX_KEY set — see .env.example",`,
499
- );
500
- });
501
- writer.write(")");
495
+ writer.write("{").newLine();
496
+ writer.writeLine(` logPrefix: "[${appName}]",`);
497
+ writer.writeLine(
498
+ ` plaintextReason: "no PLATFORM_KEK / SUBJECT_KEYS_DATABASE_URL / KUMIKO_BLIND_INDEX_KEY set — see .env.example",`,
499
+ );
500
+ writer.write("} as const");
502
501
  },
503
502
  },
504
503
  ],
505
504
  });
506
-
507
- sf.addStatements((writer) => {
508
- writer.write('if ("allowPlaintextPii" in kmsWiring) ').inlineBlock(() => {
509
- writer.writeLine(
510
- `console.warn(\`[${appName}] PII IS STORED IN PLAINTEXT — \${kmsWiring.allowPlaintextPii}\`);`,
511
- );
512
- });
505
+ sf.addVariableStatement({
506
+ declarationKind: VariableDeclarationKind.Const,
507
+ declarations: [
508
+ {
509
+ name: "kmsWiring",
510
+ initializer: (writer) => {
511
+ writer.write(`process.env["NODE_ENV"] === "production"`);
512
+ writer.newLine();
513
+ writer.write(" ? requireKmsWiring(process.env, kmsOpts)");
514
+ writer.newLine();
515
+ writer.write(" : resolveKmsWiring(process.env, kmsOpts)");
516
+ },
517
+ },
518
+ ],
513
519
  });
514
520
 
515
521
  sf.addStatements((writer) => {
@@ -697,11 +703,14 @@ JWT_SECRET=change-me-min-32-chars-change-me-min-32
697
703
  # Generate with: openssl rand -base64 32
698
704
  KUMIKO_SECRETS_MASTER_KEY_V1=
699
705
 
700
- # Subject-keys KMS for PII fields (user, tenant-invitation, fileRef). Unset by
701
- # default bin/main.ts then boots with plaintext PII and a loud warning, fine
702
- # to get running locally. Set all three before a real deploy (crypto-shredding
703
- # needs it for GDPR erasure): PLATFORM_KEK generate with: openssl rand -base64 32
706
+ # Subject-keys KMS for PII fields (user, tenant-invitation, fileRef). All-or-none.
707
+ # Unset local boot with plaintext PII + warning; NODE_ENV=production requires
708
+ # the trio (requireKmsWiring). Generate PLATFORM_KEK / KUMIKO_BLIND_INDEX_KEY
709
+ # with: openssl rand -base64 32. SUBJECT_KEYS_DATABASE_URL must be a dedicated
710
+ # subject-keys cluster (not the app DB) — see kms-adapter.md.
704
711
  PLATFORM_KEK=
712
+ # Separate Postgres instance for subject keys — never DATABASE_URL (retention
713
+ # must outlive app-DB backups for crypto-shredding).
705
714
  SUBJECT_KEYS_DATABASE_URL=
706
715
  KUMIKO_BLIND_INDEX_KEY=
707
716
 
@@ -215,7 +215,6 @@ export function renderDemoTasksIndex(): string {
215
215
 
216
216
  export function renderDemoSeedFile(): string {
217
217
  return `// Demo seed — a few tasks so \`bun dev\` shows a non-empty list.
218
- // Idempotent: skips when the tenant already has tasks (persistent dev DB).
219
218
 
220
219
  import type { SeedFn } from "@cosmicdrift/kumiko-dev-server";
221
220
  import { TestUsers } from "@cosmicdrift/kumiko-framework/stack";