@cosmicdrift/kumiko-server-runtime 0.163.3 → 0.165.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-server-runtime",
3
- "version": "0.163.3",
3
+ "version": "0.165.0",
4
4
  "description": "Production server-boot runtime for Kumiko apps: connections, schema-drift-gate, seeds, lifecycle, graceful shutdown. Symmetric to kumiko-dev-server's runDevApp, without dev/scaffold/codegen tooling.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -72,8 +72,8 @@
72
72
  }
73
73
  },
74
74
  "dependencies": {
75
- "@cosmicdrift/kumiko-bundled-features": "0.163.3",
76
- "@cosmicdrift/kumiko-framework": "0.163.3",
75
+ "@cosmicdrift/kumiko-bundled-features": "0.165.0",
76
+ "@cosmicdrift/kumiko-framework": "0.165.0",
77
77
  "temporal-polyfill": "^0.3.2"
78
78
  },
79
79
  "publishConfig": {
@@ -5,12 +5,10 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
5
5
  import { existsSync } from "node:fs";
6
6
  import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
7
7
  import { tmpdir } from "node:os";
8
- import { dirname, join, resolve } from "node:path";
9
- import { fileURLToPath } from "node:url";
8
+ import { join, resolve } from "node:path";
10
9
  import { buildProdBundle } from "../build-prod-bundle";
11
10
 
12
- const __dirname = dirname(fileURLToPath(import.meta.url));
13
- const REPO_ROOT = resolve(__dirname, "../../../..");
11
+ const REPO_ROOT = resolve(import.meta.dir, "../../../..");
14
12
 
15
13
  describe("buildProdBundle in-process (Bun.build)", () => {
16
14
  let tmp = "";
@@ -87,6 +87,11 @@ describe("composeFeatures", () => {
87
87
  });
88
88
  const toggle = features.find((f) => f.name === "auth-self-registration");
89
89
  expect(toggle).toBeDefined();
90
+ // The security-relevant part isn't that the feature is mounted, it's
91
+ // that self-signup defaults ON — a flip to `default: false` upstream
92
+ // would silently no-op signup (always-200 anti-enumeration masks it)
93
+ // while this assertion alone (toBeDefined) stayed green.
94
+ expect(toggle?.toggleableDefault).toBe(true);
90
95
  });
91
96
 
92
97
  test("no authOptions.signup → auth-self-registration NOT mounted", () => {
@@ -94,6 +99,16 @@ describe("composeFeatures", () => {
94
99
  expect(features.map((f) => f.name)).not.toContain("auth-self-registration");
95
100
  });
96
101
 
102
+ test("authOptions.signup + app also mounts its own auth-self-registration stub → deduped to exactly one", () => {
103
+ const pickerSelfRegDupe = defineFeature("auth-self-registration", () => {});
104
+ const features = composeFeatures([noopFeature, pickerSelfRegDupe], {
105
+ includeBundled: true,
106
+ authOptions: { signup: { appUrl: "https://app/signup/complete" } },
107
+ });
108
+ const names = features.map((f) => f.name).filter((n) => n === "auth-self-registration");
109
+ expect(names).toEqual(["auth-self-registration"]);
110
+ });
111
+
97
112
  test("OHNE authOptions → KEINE reset/verify-handlers (anti-default-deploy-bug)", () => {
98
113
  // Genau der Bug der vom Review-Agent gefangen wurde: composeFeatures
99
114
  // ohne authOptions registriert die handler nicht. Wenn jemand das
@@ -1,7 +1,7 @@
1
1
  // Unit coverage for mimeTypeFor + hostDispatch edge in buildStaticFallback.
2
2
 
3
3
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
4
- import { mkdtemp, rm, writeFile } from "node:fs/promises";
4
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
5
5
  import { tmpdir } from "node:os";
6
6
  import { join } from "node:path";
7
7
  import {
@@ -57,6 +57,12 @@ describe("readStaticFile / serveDiskFile", () => {
57
57
  expect(await readStaticFile(join(tmp, "missing.css"))).toBeUndefined();
58
58
  });
59
59
 
60
+ test("readStaticFile on a directory → undefined (EISDIR), not a throw", async () => {
61
+ const dirPath = join(tmp, "sub");
62
+ await mkdir(dirPath);
63
+ expect(await readStaticFile(dirPath)).toBeUndefined();
64
+ });
65
+
60
66
  test("serveDiskFile sets content-type from mime", async () => {
61
67
  const path = join(tmp, "a.svg");
62
68
  await writeFile(path, "<svg/>");
@@ -126,6 +132,15 @@ describe("buildStaticFallback hostDispatch", () => {
126
132
  expect(await res.text()).toBe("PNGDATA");
127
133
  });
128
134
 
135
+ test("a request for a directory copied verbatim from public/ falls back to index.html instead of 500ing", async () => {
136
+ await mkdir(join(tmp, "sub"));
137
+ await writeFile(join(tmp, "index.html"), "<!doctype html><html><body>spa-shell</body></html>");
138
+ const handler = buildStaticFallback(() => new Response("404", { status: 404 }), tmp, "{}");
139
+ const res = await handler(new Request("http://t/sub"));
140
+ expect(res.status).toBe(200);
141
+ expect(await res.text()).toContain("spa-shell");
142
+ });
143
+
129
144
  test("hostDispatch html with CSP + Vary: Host", async () => {
130
145
  await writeFile(join(tmp, "tenant.html"), "<!doctype html><html><body>ok</body></html>");
131
146
  const handler = buildStaticFallback(
@@ -836,8 +836,8 @@ describe("runProdApp — auth allowedOrigins forwarding", () => {
836
836
 
837
837
  test("cookieDomain + allowedOrigins clears the guard — allowlist reaches buildServer", async () => {
838
838
  // Without the forwarding fix this would ALSO throw /allowedOrigins is empty/.
839
- // It may still fail later on the minimal harness (no auth tables migrated),
840
- // but never on the origin guard that is the forwarding proof.
839
+ // auth tables are migrated for this file (migrateTestDb() above); this test
840
+ // isolates only that a later boot failure is never the origin-guard message.
841
841
  let bootError: unknown;
842
842
  try {
843
843
  const handle = await boot(undefined, {
@@ -1,15 +1,17 @@
1
- // composeFeatures — single source of truth für die Feature-Liste die
2
- // Boot UND Schema-Generator sehen.
1
+ // composeFeatures — single source of truth for the feature list that
2
+ // boot AND the schema generator see.
3
3
  //
4
- // Sowohl runDevApp als auch runProdApp mischen im auth-mode dieselben
5
- // vier Bundled-Features dazu (config + user + tenant + auth-email-pw).
6
- // Damit der drizzle-Schema-Generator pro App genau dieselbe Feature-
7
- // Liste sieht wie die Runtime, leben die Komposition hier beide
8
- // Bootstrap-Wrapper UND der per-app drizzle/generate.ts rufen sie auf.
4
+ // Both runDevApp and runProdApp mix in the same bundled features in
5
+ // auth-mode (config + user + tenant + auth-email-password, plus
6
+ // auth-self-registration when authOptions.signup is set). So the
7
+ // drizzle schema generator sees the exact same feature list per app as
8
+ // the runtime, the composition lives here both bootstrap wrappers AND
9
+ // each app's drizzle/generate.ts call it.
9
10
  //
10
- // Reihenfolge: Infrastruktur-Features (config/user/tenant) zuerst, dann
11
- // auth-email-password, dann die App-Features. Spätere Features dürfen
12
- // auf Frühere referenzieren (z.B. authClaims-Hooks an user/tenant).
11
+ // Order: infrastructure features (config/user/tenant) first, then
12
+ // auth-email-password (+ auth-self-registration when authOptions.signup
13
+ // is set), then the app features. Later features may reference earlier
14
+ // ones (e.g. authClaims hooks on user/tenant).
13
15
 
14
16
  import {
15
17
  type AccountUnlockOptions,
@@ -33,7 +35,8 @@ import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
33
35
 
34
36
  export type ComposeFeaturesOptions = {
35
37
  /** When true, prepends config + user + tenant + auth-email-password
36
- * before the app features. Mirror of "auth-mode" in run{Dev,Prod}App. */
38
+ * (+ auth-self-registration when authOptions.signup is set) before the
39
+ * app features. Mirror of "auth-mode" in run{Dev,Prod}App. */
37
40
  readonly includeBundled: boolean;
38
41
  /** Optional auth-feature-options durchgereicht an
39
42
  * createAuthEmailPasswordFeature. Wenn passwordReset / emailVerification
@@ -43,7 +43,12 @@ export async function readStaticFile(
43
43
  const [bytes, fileStat] = await Promise.all([readFile(filePath), stat(filePath)]);
44
44
  return { bytes, mime: mimeTypeFor(filePath), mtimeMs: fileStat.mtimeMs };
45
45
  } catch (err) {
46
- if ((err as { code?: string }).code === "ENOENT") return undefined;
46
+ const code = (err as { code?: string }).code;
47
+ // ENOENT: no such path. EISDIR: readFile() on a directory (e.g. GET
48
+ // /assets where "assets" is a subfolder copied verbatim from public/)
49
+ // — both mean "not a servable file", so fall through to the SPA
50
+ // fallback instead of a 500.
51
+ if (code === "ENOENT" || code === "EISDIR") return undefined;
47
52
  throw err;
48
53
  }
49
54
  }