@cosmicdrift/kumiko-server-runtime 0.164.0 → 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 +3 -3
- package/src/__tests__/build-prod-bundle.inprocess.test.ts +2 -4
- package/src/__tests__/compose-features.test.ts +15 -0
- package/src/__tests__/run-prod-app-static-files.test.ts +16 -1
- package/src/__tests__/run-prod-app.integration.test.ts +2 -2
- package/src/compose-features.ts +14 -11
- package/src/run-prod-app-static-files.ts +6 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-server-runtime",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
76
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
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 {
|
|
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
|
|
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
|
-
//
|
|
840
|
-
//
|
|
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, {
|
package/src/compose-features.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
// composeFeatures — single source of truth
|
|
2
|
-
//
|
|
1
|
+
// composeFeatures — single source of truth for the feature list that
|
|
2
|
+
// boot AND the schema generator see.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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
|
-
//
|
|
11
|
-
// auth-email-password
|
|
12
|
-
//
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
}
|