@cosmicdrift/kumiko-framework 0.261.0 → 0.262.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/changes.json +7 -0
- package/src/engine/boot-validator/__tests__/security-baseline.test.ts +107 -0
- package/src/engine/boot-validator/index.ts +3 -0
- package/src/engine/boot-validator/security-baseline.ts +26 -0
- package/src/engine/boot-validator.ts +1 -0
- package/src/engine/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.262.0",
|
|
4
4
|
"description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -198,7 +198,7 @@
|
|
|
198
198
|
"./package.json": "./package.json"
|
|
199
199
|
},
|
|
200
200
|
"dependencies": {
|
|
201
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
201
|
+
"@cosmicdrift/kumiko-types": "0.262.0",
|
|
202
202
|
"bullmq": "^5.76.7",
|
|
203
203
|
"bun-types": "^1.3.13",
|
|
204
204
|
"hono": "^4.13.1",
|
|
@@ -214,7 +214,7 @@
|
|
|
214
214
|
"zod": "^4.4.3"
|
|
215
215
|
},
|
|
216
216
|
"devDependencies": {
|
|
217
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
217
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.262.0",
|
|
218
218
|
"bun-types": "^1.3.13",
|
|
219
219
|
"pino-pretty": "^13.1.3"
|
|
220
220
|
},
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.262.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Boot validator warns when NODE_ENV=production boots without the security baseline (fw#2857).",
|
|
6
|
+
"detail": "`SECURITY_BASELINE_FEATURE_NAMES` (`@cosmicdrift/kumiko-framework/engine`) names the four security-relevant feature names the boot validator expects in a production app: `sessions`, `crypto-shredding`, `rate-limiting`, `audit`. `validateBoot` now calls `warnOnMissingSecurityBaseline(features)` (`engine/boot-validator/security-baseline.ts`) after every other check; when `process.env.NODE_ENV === \"production\"` and one or more of those feature names is absent from the mounted feature list (matched by exact `FeatureDefinition.name`, not by require-chain inference), it logs one `console.warn` naming the missing feature(s) and pointing at `securityBaselineFeatures()`. It never throws and is a no-op for any other `NODE_ENV` value (including `undefined`). Mount `securityBaselineFeatures()` from `@cosmicdrift/kumiko-bundled-features/presets` to silence it.",
|
|
7
|
+
"migration": "No action required for existing apps outside production. Apps that boot with `NODE_ENV=production` and don't already mount `sessions`/`crypto-shredding`/`rate-limiting`/`audit` will see a new boot-time `console.warn` — mount the missing feature(s) individually or via `securityBaselineFeatures()` to remove it."
|
|
8
|
+
},
|
|
2
9
|
{
|
|
3
10
|
"version": "0.261.0",
|
|
4
11
|
"type": "breaking",
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
|
|
2
|
+
import { defineFeature } from "../../define-feature";
|
|
3
|
+
import type { FeatureDefinition } from "../../types";
|
|
4
|
+
import { validateBoot } from "../index";
|
|
5
|
+
import {
|
|
6
|
+
SECURITY_BASELINE_FEATURE_NAMES,
|
|
7
|
+
warnOnMissingSecurityBaseline,
|
|
8
|
+
} from "../security-baseline";
|
|
9
|
+
|
|
10
|
+
function stubFeature(name: string): FeatureDefinition {
|
|
11
|
+
return defineFeature(name, () => {});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("warnOnMissingSecurityBaseline", () => {
|
|
15
|
+
let warnSpy: ReturnType<typeof spyOn<Console, "warn">>;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
warnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
warnSpy.mockRestore();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("production + no baseline features → warns once, naming all four", () => {
|
|
26
|
+
warnOnMissingSecurityBaseline([], "production");
|
|
27
|
+
|
|
28
|
+
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
29
|
+
const msg = String(warnSpy.mock.calls[0]?.[0]);
|
|
30
|
+
for (const name of SECURITY_BASELINE_FEATURE_NAMES) {
|
|
31
|
+
expect(msg).toContain(name);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("production + only sessions/audit mounted → warns about the missing two only", () => {
|
|
36
|
+
const features = [stubFeature("sessions"), stubFeature("audit")];
|
|
37
|
+
|
|
38
|
+
warnOnMissingSecurityBaseline(features, "production");
|
|
39
|
+
|
|
40
|
+
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
41
|
+
const msg = String(warnSpy.mock.calls[0]?.[0]);
|
|
42
|
+
expect(msg).toContain("crypto-shredding");
|
|
43
|
+
expect(msg).toContain("rate-limiting");
|
|
44
|
+
expect(msg).not.toContain("missing feature(s): sessions");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("production + all four baseline features mounted → no warning", () => {
|
|
48
|
+
const features = SECURITY_BASELINE_FEATURE_NAMES.map((name) => stubFeature(name));
|
|
49
|
+
|
|
50
|
+
warnOnMissingSecurityBaseline(features, "production");
|
|
51
|
+
|
|
52
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test.each(["development", "test", undefined])(
|
|
56
|
+
"nodeEnv=%p + no baseline features → no warning",
|
|
57
|
+
(nodeEnv) => {
|
|
58
|
+
warnOnMissingSecurityBaseline([], nodeEnv);
|
|
59
|
+
|
|
60
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
test("never throws, regardless of nodeEnv or mounted features", () => {
|
|
65
|
+
expect(() => warnOnMissingSecurityBaseline([], "production")).not.toThrow();
|
|
66
|
+
expect(() => warnOnMissingSecurityBaseline([], "development")).not.toThrow();
|
|
67
|
+
expect(() =>
|
|
68
|
+
warnOnMissingSecurityBaseline(
|
|
69
|
+
SECURITY_BASELINE_FEATURE_NAMES.map((name) => stubFeature(name)),
|
|
70
|
+
"production",
|
|
71
|
+
),
|
|
72
|
+
).not.toThrow();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("validateBoot + security baseline warning", () => {
|
|
77
|
+
const originalNodeEnv = process.env["NODE_ENV"];
|
|
78
|
+
let warnSpy: ReturnType<typeof spyOn<Console, "warn">>;
|
|
79
|
+
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
warnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
afterEach(() => {
|
|
85
|
+
warnSpy.mockRestore();
|
|
86
|
+
if (originalNodeEnv === undefined) {
|
|
87
|
+
delete process.env["NODE_ENV"];
|
|
88
|
+
} else {
|
|
89
|
+
process.env["NODE_ENV"] = originalNodeEnv;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("production boot with all four baseline stubs mounted does not throw and does not warn", () => {
|
|
94
|
+
process.env["NODE_ENV"] = "production";
|
|
95
|
+
const features = SECURITY_BASELINE_FEATURE_NAMES.map((name) => stubFeature(name));
|
|
96
|
+
|
|
97
|
+
expect(() => validateBoot(features)).not.toThrow();
|
|
98
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("production boot with no baseline features mounted warns but does not throw", () => {
|
|
102
|
+
process.env["NODE_ENV"] = "production";
|
|
103
|
+
|
|
104
|
+
expect(() => validateBoot([])).not.toThrow();
|
|
105
|
+
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
validateScreenShortIdCollisions,
|
|
62
62
|
validateScreens,
|
|
63
63
|
} from "./screens";
|
|
64
|
+
import { warnOnMissingSecurityBaseline } from "./security-baseline";
|
|
64
65
|
import {
|
|
65
66
|
collectWorkspaceQns,
|
|
66
67
|
validateDefaultWorkspaceUniqueness,
|
|
@@ -72,6 +73,7 @@ export { validateAppCustomScreenWriteQns } from "./custom-screen-write-qns";
|
|
|
72
73
|
// an den Codegen zu übergeben. Nicht Teil von validateBoot, aber
|
|
73
74
|
// dieselbe Extraktionslogik.
|
|
74
75
|
export { collectWriteHandlerQns } from "./nav";
|
|
76
|
+
export { SECURITY_BASELINE_FEATURE_NAMES } from "./security-baseline";
|
|
75
77
|
|
|
76
78
|
export type ValidateBootOptions = {
|
|
77
79
|
/** Warn when an access role is used by exactly one handler/config-key/
|
|
@@ -254,6 +256,7 @@ export function validateBoot(
|
|
|
254
256
|
|
|
255
257
|
validateConfigReads(features, allConfigKeys);
|
|
256
258
|
warnOnToggleableDependencies(features, featureMap);
|
|
259
|
+
warnOnMissingSecurityBaseline(features);
|
|
257
260
|
if (options?.warnOnUniqueAccessRoles === true) {
|
|
258
261
|
warnOnUniqueAccessRoles(features);
|
|
259
262
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { FeatureDefinition } from "../types";
|
|
2
|
+
|
|
3
|
+
export const SECURITY_BASELINE_FEATURE_NAMES = [
|
|
4
|
+
"sessions",
|
|
5
|
+
"crypto-shredding",
|
|
6
|
+
"rate-limiting",
|
|
7
|
+
"audit",
|
|
8
|
+
] as const;
|
|
9
|
+
|
|
10
|
+
export function warnOnMissingSecurityBaseline(
|
|
11
|
+
features: readonly FeatureDefinition[],
|
|
12
|
+
nodeEnv: string | undefined = process.env["NODE_ENV"],
|
|
13
|
+
): void {
|
|
14
|
+
// skip: baseline is only enforced for production boots
|
|
15
|
+
if (nodeEnv !== "production") return;
|
|
16
|
+
|
|
17
|
+
const mountedNames = new Set(features.map((f) => f.name));
|
|
18
|
+
const missing = SECURITY_BASELINE_FEATURE_NAMES.filter((name) => !mountedNames.has(name));
|
|
19
|
+
// skip: every baseline feature is mounted
|
|
20
|
+
if (missing.length === 0) return;
|
|
21
|
+
|
|
22
|
+
// biome-ignore lint/suspicious/noConsole: boot-time hint, no logger available yet
|
|
23
|
+
console.warn(
|
|
24
|
+
`[kumiko:boot] NODE_ENV=production but the security baseline is incomplete — missing feature(s): ${missing.join(", ")}. Mount securityBaselineFeatures() from @cosmicdrift/kumiko-bundled-features/presets (or the missing features individually).`,
|
|
25
|
+
);
|
|
26
|
+
}
|