@cosmicdrift/kumiko-dev-server 0.328.1 → 0.330.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/README.md +17 -0
- package/bin/kumiko-build.ts +3 -2
- package/bin/kumiko-dev.ts +1 -1
- package/bin/kumiko-init-deploy.ts +1 -1
- package/bin/kumiko-schema-check.ts +4 -1
- package/dist/build-server-bundle.d.ts +31 -0
- package/dist/build-server-bundle.js +282 -0
- package/dist/build.d.ts +2 -0
- package/dist/build.js +5 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +9 -0
- package/dist/codegen/index.d.ts +4 -0
- package/dist/codegen/index.js +4 -0
- package/dist/codegen/render.d.ts +55 -0
- package/dist/codegen/render.js +273 -0
- package/dist/codegen/run-codegen.d.ts +20 -0
- package/dist/codegen/run-codegen.js +219 -0
- package/dist/codegen/scan-events.d.ts +76 -0
- package/dist/codegen/scan-events.js +447 -0
- package/dist/codegen/watch.d.ts +33 -0
- package/dist/codegen/watch.js +112 -0
- package/dist/compose-stacks.d.ts +60 -0
- package/dist/compose-stacks.js +127 -0
- package/dist/crash-tracker.d.ts +17 -0
- package/dist/crash-tracker.js +37 -0
- package/dist/create-kumiko-server.d.ts +195 -0
- package/dist/create-kumiko-server.js +1040 -0
- package/dist/env-schema.d.ts +15 -0
- package/dist/env-schema.js +60 -0
- package/dist/few-shot-corpus.d.ts +72 -0
- package/dist/few-shot-corpus.js +262 -0
- package/dist/framework-version.d.ts +1 -0
- package/dist/framework-version.js +31 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +33 -0
- package/dist/init-deploy-cli.d.ts +10 -0
- package/dist/init-deploy-cli.js +111 -0
- package/dist/kebab.d.ts +1 -0
- package/{src/kebab.ts → dist/kebab.js} +2 -3
- package/dist/run-dev-app.d.ts +174 -0
- package/dist/run-dev-app.js +403 -0
- package/dist/scaffold-app-feature.d.ts +16 -0
- package/dist/scaffold-app-feature.js +240 -0
- package/dist/scaffold-app.d.ts +45 -0
- package/dist/scaffold-app.js +852 -0
- package/dist/scaffold-demo-tasks.d.ts +8 -0
- package/{src/scaffold-demo-tasks.ts → dist/scaffold-demo-tasks.js} +69 -98
- package/dist/scaffold-deploy.d.ts +92 -0
- package/dist/scaffold-deploy.js +254 -0
- package/dist/scaffold-feature.d.ts +26 -0
- package/dist/scaffold-feature.js +163 -0
- package/dist/schema-apply.d.ts +11 -0
- package/dist/schema-apply.js +95 -0
- package/dist/schema-check-core.d.ts +2 -0
- package/{src/schema-check-core.ts → dist/schema-check-core.js} +8 -10
- package/dist/schema-tables-auth-mode.d.ts +2 -0
- package/{src/schema-tables-auth-mode.ts → dist/schema-tables-auth-mode.js} +2 -10
- package/dist/schema-tables-minimal.d.ts +2 -0
- package/{src/schema-tables-minimal.ts → dist/schema-tables-minimal.js} +2 -11
- package/dist/setup-test-stack-from-features.d.ts +11 -0
- package/dist/setup-test-stack-from-features.js +42 -0
- package/dist/welcome-banner.d.ts +15 -0
- package/dist/welcome-banner.js +27 -0
- package/package.json +71 -24
- package/src/changes.json +6 -0
- package/src/__tests__/build-prod-bundle.integration.test.ts +0 -282
- package/src/__tests__/build-server-bundle.test.ts +0 -228
- package/src/__tests__/classify-change.test.ts +0 -87
- package/src/__tests__/compose-stacks.test.ts +0 -273
- package/src/__tests__/config-resolver-default.integration.test.ts +0 -165
- package/src/__tests__/crash-tracker.test.ts +0 -89
- package/src/__tests__/create-kumiko-server-errors.test.ts +0 -54
- package/src/__tests__/create-kumiko-server-options.test.ts +0 -33
- package/src/__tests__/create-kumiko-server.integration.test.ts +0 -1007
- package/src/__tests__/discover-format.test.ts +0 -32
- package/src/__tests__/env-schema.integration.test.ts +0 -248
- package/src/__tests__/env-schema.test.ts +0 -132
- package/src/__tests__/few-shot-corpus.test.ts +0 -316
- package/src/__tests__/framework-version.test.ts +0 -45
- package/src/__tests__/init-deploy-cli.test.ts +0 -113
- package/src/__tests__/kumiko-schema-check.test.ts +0 -56
- package/src/__tests__/merge-extra-context.test.ts +0 -67
- package/src/__tests__/resolve-stylesheet.test.ts +0 -132
- package/src/__tests__/run-dev-app-boot-validation.test.ts +0 -38
- package/src/__tests__/run-dev-app.integration.test.ts +0 -290
- package/src/__tests__/runtime-classification.test.ts +0 -26
- package/src/__tests__/saas-identity-wire.integration.test.ts +0 -427
- package/src/__tests__/scaffold-app-feature.test.ts +0 -216
- package/src/__tests__/scaffold-app.test.ts +0 -630
- package/src/__tests__/scaffold-demo-tasks.test.ts +0 -47
- package/src/__tests__/scaffold-deploy.test.ts +0 -464
- package/src/__tests__/scaffold-dev-cycle.integration.test.ts +0 -109
- package/src/__tests__/scaffold-feature.test.ts +0 -143
- package/src/__tests__/schema-apply.integration.test.ts +0 -191
- package/src/__tests__/setup-test-stack-from-features.integration.test.ts +0 -30
- package/src/__tests__/setup-test-stack-job-run-logger.integration.test.ts +0 -72
- package/src/__tests__/walkthrough.integration.test.ts +0 -140
- package/src/__tests__/welcome-banner.test.ts +0 -42
- package/src/build-server-bundle.ts +0 -346
- package/src/build.ts +0 -28
- package/src/codegen/__tests__/render-codegen.test.ts +0 -100
- package/src/codegen/__tests__/run-codegen.test.ts +0 -665
- package/src/codegen/__tests__/strict-mode-diagnostics.test.ts +0 -347
- package/src/codegen/__tests__/watch.test.ts +0 -222
- package/src/codegen/index.ts +0 -19
- package/src/codegen/render.ts +0 -305
- package/src/codegen/run-codegen.ts +0 -278
- package/src/codegen/scan-events.ts +0 -565
- package/src/codegen/watch.ts +0 -140
- package/src/compose-stacks.ts +0 -193
- package/src/crash-tracker.ts +0 -56
- package/src/create-kumiko-server.ts +0 -1397
- package/src/env-schema.ts +0 -72
- package/src/few-shot-corpus.ts +0 -370
- package/src/framework-version.ts +0 -39
- package/src/index.ts +0 -105
- package/src/init-deploy-cli.ts +0 -148
- package/src/run-dev-app.ts +0 -694
- package/src/scaffold-app-feature.ts +0 -283
- package/src/scaffold-app.ts +0 -1033
- package/src/scaffold-deploy.ts +0 -393
- package/src/scaffold-feature.ts +0 -226
- package/src/schema-apply.ts +0 -123
- package/src/setup-test-stack-from-features.ts +0 -80
- package/src/welcome-banner.ts +0 -43
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
// Unit-Tests für resolveStylesheet — die Verzweigungen aus
|
|
2
|
-
// createKumikoServer's CSS-Setup. Läuft unter vitest auf Node, also
|
|
3
|
-
// ohne Bun.serve und ohne Bun.resolveSync; der Default-Resolution-
|
|
4
|
-
// Pfad (Bun-only) ist hier explizit als "skip silent" abgedeckt.
|
|
5
|
-
//
|
|
6
|
-
// Warum als eigener File statt im create-kumiko-server.integration.ts:
|
|
7
|
-
// die anderen Tests dort booten ein TestStack (DB + Redis), das brauchen
|
|
8
|
-
// wir für reine Resolver-Logik nicht.
|
|
9
|
-
|
|
10
|
-
import { describe, expect, test } from "bun:test";
|
|
11
|
-
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
12
|
-
import { tmpdir } from "node:os";
|
|
13
|
-
import { join } from "node:path";
|
|
14
|
-
import {
|
|
15
|
-
resolveStylesheet,
|
|
16
|
-
resolveStylesheetWatch,
|
|
17
|
-
STYLESHEET_WATCH_ENV,
|
|
18
|
-
} from "../create-kumiko-server";
|
|
19
|
-
|
|
20
|
-
describe("resolveStylesheet", () => {
|
|
21
|
-
test("string → resolved absolute path", () => {
|
|
22
|
-
// Relativ zum CWD aufgelöst — das ist Node's path.resolve-Default.
|
|
23
|
-
const out = resolveStylesheet({
|
|
24
|
-
features: [],
|
|
25
|
-
stylesheet: "./some/stylesheet.css",
|
|
26
|
-
});
|
|
27
|
-
expect(out).toMatch(/\/some\/stylesheet\.css$/);
|
|
28
|
-
expect(out?.startsWith("/")).toBe(true);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test("absolute string bleibt unverändert (path.resolve idempotent)", () => {
|
|
32
|
-
const out = resolveStylesheet({
|
|
33
|
-
features: [],
|
|
34
|
-
stylesheet: "/abs/path/styles.css",
|
|
35
|
-
});
|
|
36
|
-
expect(out).toBe("/abs/path/styles.css");
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test("false → undefined (Pipeline explizit aus)", () => {
|
|
40
|
-
const out = resolveStylesheet({
|
|
41
|
-
features: [],
|
|
42
|
-
stylesheet: false,
|
|
43
|
-
clientEntry: "./entry.tsx",
|
|
44
|
-
});
|
|
45
|
-
expect(out).toBeUndefined();
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test("undefined ohne clientEntry → undefined (kein Browser-Bundle, keine CSS)", () => {
|
|
49
|
-
const out = resolveStylesheet({
|
|
50
|
-
features: [],
|
|
51
|
-
});
|
|
52
|
-
expect(out).toBeUndefined();
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test("undefined + clientEntry: undefined (silent skip)", () => {
|
|
56
|
-
// Bun resolveSync findet packages/renderer-web/src/styles.css im
|
|
57
|
-
// Repo. Das ist der prod-Pfad — unter Bun (real oder CI) ist der
|
|
58
|
-
// Wert ein absoluter path; unter Node (von wo der Test portiert
|
|
59
|
-
// wurde) wäre es undefined. Wir akzeptieren beides.
|
|
60
|
-
const out = resolveStylesheet({
|
|
61
|
-
features: [],
|
|
62
|
-
clientEntry: "./entry.tsx",
|
|
63
|
-
});
|
|
64
|
-
if (typeof Bun === "undefined") {
|
|
65
|
-
expect(out).toBeUndefined();
|
|
66
|
-
} else {
|
|
67
|
-
expect(out).toMatch(/renderer-web\/src\/styles\.css$/);
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("Bun.resolveSync failure → undefined (catch path, no throw)", () => {
|
|
72
|
-
const tmpDir = realpathSync(mkdtempSync(join(tmpdir(), "kumiko-resolve-catch-")));
|
|
73
|
-
const cwdBefore = process.cwd();
|
|
74
|
-
process.chdir(tmpDir);
|
|
75
|
-
try {
|
|
76
|
-
const out = resolveStylesheet({
|
|
77
|
-
features: [],
|
|
78
|
-
clientEntry: "./entry.tsx",
|
|
79
|
-
});
|
|
80
|
-
expect(out).toBeUndefined();
|
|
81
|
-
} finally {
|
|
82
|
-
process.chdir(cwdBefore);
|
|
83
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
test("undefined + clientEntry + src/styles.css existiert → returns App-Theme-Override", () => {
|
|
88
|
-
// Auto-Detection greift VOR dem renderer-web-Fallback: Wenn die App
|
|
89
|
-
// ein eigenes src/styles.css hat (App-Theme-Pattern), wird das
|
|
90
|
-
// automatisch als Tailwind-Entry genommen — symmetrisch zum prod-
|
|
91
|
-
// Build (kumiko-build:resolveStylesheetEntry). Ohne diesen Check
|
|
92
|
-
// müsste jede App `stylesheet:` explizit setzen.
|
|
93
|
-
// realpath aufgelöst, weil macOS' /var → /private/var Symlink sonst
|
|
94
|
-
// einen anderen Pfad rauswirft als process.cwd() nach chdir.
|
|
95
|
-
const tmpDir = realpathSync(mkdtempSync(join(tmpdir(), "kumiko-resolve-styles-")));
|
|
96
|
-
const srcDir = join(tmpDir, "src");
|
|
97
|
-
mkdirSync(srcDir);
|
|
98
|
-
const stylesheet = join(srcDir, "styles.css");
|
|
99
|
-
writeFileSync(stylesheet, "/* app theme */");
|
|
100
|
-
|
|
101
|
-
const cwdBefore = process.cwd();
|
|
102
|
-
process.chdir(tmpDir);
|
|
103
|
-
try {
|
|
104
|
-
const out = resolveStylesheet({
|
|
105
|
-
features: [],
|
|
106
|
-
clientEntry: "./entry.tsx",
|
|
107
|
-
});
|
|
108
|
-
expect(out).toBe(stylesheet);
|
|
109
|
-
} finally {
|
|
110
|
-
process.chdir(cwdBefore);
|
|
111
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
describe("resolveStylesheetWatch", () => {
|
|
117
|
-
test("an explicit option wins over the env", () => {
|
|
118
|
-
expect(resolveStylesheetWatch({ stylesheetWatch: false }, {})).toBe(false);
|
|
119
|
-
expect(resolveStylesheetWatch({ stylesheetWatch: true }, { [STYLESHEET_WATCH_ENV]: "0" })).toBe(
|
|
120
|
-
true,
|
|
121
|
-
);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test("no option, env=0 → false", () => {
|
|
125
|
-
expect(resolveStylesheetWatch({}, { [STYLESHEET_WATCH_ENV]: "0" })).toBe(false);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
test("no option, no or other env value → true", () => {
|
|
129
|
-
expect(resolveStylesheetWatch({}, {})).toBe(true);
|
|
130
|
-
expect(resolveStylesheetWatch({}, { [STYLESHEET_WATCH_ENV]: "1" })).toBe(true);
|
|
131
|
-
});
|
|
132
|
-
});
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
// runDevApp muss denselben Boot-Validator wie runProdApp ausführen (#359):
|
|
2
|
-
// eine ganze Fehlerklasse (unqualifizierte nav-/handler-QNs, unauflösbare
|
|
3
|
-
// navigate-Targets, screen-access) passierte früher den Dev-Server still und
|
|
4
|
-
// crashte erst den Prod-Pod im CrashLoopBackOff. Hier: ein Feature mit einem
|
|
5
|
-
// rowAction-navigate auf einen nie registrierten Screen — runDevApp muss
|
|
6
|
-
// SYNCHRON beim Boot werfen, bevor ein Port gebunden oder der codegen-Watcher
|
|
7
|
-
// gestartet wird (validateBoot läuft vor watchAndRegenerate).
|
|
8
|
-
|
|
9
|
-
import { describe, expect, test } from "bun:test";
|
|
10
|
-
import { createEntity, createTextField, defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
11
|
-
import { runDevApp } from "../run-dev-app";
|
|
12
|
-
|
|
13
|
-
function unresolvableNavFeature() {
|
|
14
|
-
return defineFeature("shop", (r) => {
|
|
15
|
-
r.entity(
|
|
16
|
-
"product",
|
|
17
|
-
createEntity({
|
|
18
|
-
fields: { name: createTextField({ personal: false, reason: "test_fixture" }) },
|
|
19
|
-
}),
|
|
20
|
-
);
|
|
21
|
-
r.screen({
|
|
22
|
-
id: "product-list",
|
|
23
|
-
type: "entityList",
|
|
24
|
-
entity: "product",
|
|
25
|
-
columns: ["name"],
|
|
26
|
-
// "ghost-screen" wird nie via r.screen registriert → validateBoot wirft.
|
|
27
|
-
rowActions: [{ kind: "navigate", id: "edit", label: "actions.edit", screen: "ghost-screen" }],
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
describe("runDevApp boot-validation (#359)", () => {
|
|
33
|
-
test("unresolvable navigate-target throws at boot — dev/prod parity, no port bound", async () => {
|
|
34
|
-
await expect(runDevApp({ features: [unresolvableNavFeature()] })).rejects.toThrow(
|
|
35
|
-
/navigate-target "ghost-screen" does not resolve/,
|
|
36
|
-
);
|
|
37
|
-
});
|
|
38
|
-
});
|
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
// The origin-guard config (allowedOrigins / unsafeSkipOriginCheck) is forwarded
|
|
2
|
-
// from runDevApp's auth options through to the server exactly like runProdApp
|
|
3
|
-
// (#399/1). The guard fires during server build — AFTER the ephemeral test DB is
|
|
4
|
-
// up — so this is an integration test (needs TEST_DATABASE_URL), mirroring the
|
|
5
|
-
// run-prod-app forwarding pair. Without it a typo or wrong spread-key on the dev
|
|
6
|
-
// path would silently drop the fail-closed guard and dev/prod would diverge.
|
|
7
|
-
|
|
8
|
-
import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
|
|
9
|
-
import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
10
|
-
import { createPersonalAccessTokensFeature } from "@cosmicdrift/kumiko-bundled-features/personal-access-tokens";
|
|
11
|
-
import { createSessionsFeature } from "@cosmicdrift/kumiko-bundled-features/sessions";
|
|
12
|
-
import {
|
|
13
|
-
requireTemplateResolver,
|
|
14
|
-
TEXT_BLOCK_KIND,
|
|
15
|
-
type TemplateResolverApi,
|
|
16
|
-
} from "@cosmicdrift/kumiko-bundled-features/template-resolver";
|
|
17
|
-
import type { AnonymousExtraRoute } from "@cosmicdrift/kumiko-framework/api";
|
|
18
|
-
import {
|
|
19
|
-
createEntity,
|
|
20
|
-
createTextField,
|
|
21
|
-
defineFeature,
|
|
22
|
-
defineQueryHandler,
|
|
23
|
-
type TenantId,
|
|
24
|
-
} from "@cosmicdrift/kumiko-framework/engine";
|
|
25
|
-
import { TestUsers } from "@cosmicdrift/kumiko-framework/stack";
|
|
26
|
-
import * as jose from "jose";
|
|
27
|
-
import * as z from "zod";
|
|
28
|
-
import type { KumikoServerHandle } from "../create-kumiko-server";
|
|
29
|
-
import { runDevApp } from "../run-dev-app";
|
|
30
|
-
|
|
31
|
-
function validFeature() {
|
|
32
|
-
return defineFeature("shop", (r) => {
|
|
33
|
-
r.entity(
|
|
34
|
-
"product",
|
|
35
|
-
createEntity({
|
|
36
|
-
fields: { name: createTextField({ personal: false, reason: "test_fixture" }) },
|
|
37
|
-
}),
|
|
38
|
-
);
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const ADMIN = {
|
|
43
|
-
email: "origin-guard@example.eu",
|
|
44
|
-
password: "test-pw-strong-1234",
|
|
45
|
-
displayName: "Admin",
|
|
46
|
-
memberships: [],
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
let handle: KumikoServerHandle | undefined;
|
|
50
|
-
const savedJwtSecret = process.env["JWT_SECRET"];
|
|
51
|
-
|
|
52
|
-
beforeEach(() => {
|
|
53
|
-
process.env["JWT_SECRET"] = "test-rundev-secret-32-chars-min!!!!";
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
afterEach(async () => {
|
|
57
|
-
await handle?.stop();
|
|
58
|
-
handle = undefined;
|
|
59
|
-
if (savedJwtSecret === undefined) delete process.env["JWT_SECRET"];
|
|
60
|
-
else process.env["JWT_SECRET"] = savedJwtSecret;
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
describe("runDevApp — auth.mail fail-fast on missing JWT_SECRET", () => {
|
|
64
|
-
test("options.auth set but JWT_SECRET missing → throws, no dev-fallback", async () => {
|
|
65
|
-
// beforeEach sets JWT_SECRET unconditionally for every OTHER test in this
|
|
66
|
-
// file — none of them ever exercise the missing-secret path, so it's
|
|
67
|
-
// unclear whether the fail-closed guard (requireEnv, not the old dev-
|
|
68
|
-
// fallback) actually fires or was silently dropped. Pass a custom
|
|
69
|
-
// envSource without JWT_SECRET instead of touching process.env.
|
|
70
|
-
const { JWT_SECRET: _drop, ...envWithoutJwtSecret } = process.env;
|
|
71
|
-
await expect(
|
|
72
|
-
runDevApp({
|
|
73
|
-
features: [validFeature()],
|
|
74
|
-
auth: { admin: ADMIN, cookieDomain: "example.eu", allowedOrigins: ["https://example.eu"] },
|
|
75
|
-
envSource: envWithoutJwtSecret,
|
|
76
|
-
}),
|
|
77
|
-
).rejects.toThrow(/JWT_SECRET/);
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
describe("runDevApp — auth allowedOrigins forwarding (#399/1)", () => {
|
|
82
|
-
test("cookieDomain without allowedOrigins fails closed — guard is wired through runDevApp", async () => {
|
|
83
|
-
await expect(
|
|
84
|
-
runDevApp({
|
|
85
|
-
features: [validFeature()],
|
|
86
|
-
auth: { admin: ADMIN, cookieDomain: "example.eu" },
|
|
87
|
-
}),
|
|
88
|
-
).rejects.toThrow(/allowedOrigins is empty/);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test("cookieDomain + allowedOrigins clears the guard — allowlist reaches the server", async () => {
|
|
92
|
-
// Without the forwarding fix this would ALSO throw /allowedOrigins is empty/.
|
|
93
|
-
handle = await runDevApp({
|
|
94
|
-
features: [validFeature()],
|
|
95
|
-
// Port 0 → OS-assigned ephemeral port, no fixed-port collision in CI.
|
|
96
|
-
port: 0,
|
|
97
|
-
auth: {
|
|
98
|
-
admin: ADMIN,
|
|
99
|
-
cookieDomain: "example.eu",
|
|
100
|
-
allowedOrigins: ["https://app.example.eu"],
|
|
101
|
-
},
|
|
102
|
-
});
|
|
103
|
-
// A booted handle that did not throw on the origin guard IS the forwarding
|
|
104
|
-
// proof — the allowlist reached the server build.
|
|
105
|
-
expect(handle).toBeDefined();
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test("cookieDomain + unsafeSkipOriginCheck clears the guard without an allowlist", async () => {
|
|
109
|
-
// The escape hatch must also forward: cookieDomain alone fails closed
|
|
110
|
-
// (first test), but unsafeSkipOriginCheck=true bypasses it. A dropped/
|
|
111
|
-
// mis-spread key would let the guard fire → /allowedOrigins is empty/.
|
|
112
|
-
handle = await runDevApp({
|
|
113
|
-
features: [validFeature()],
|
|
114
|
-
port: 0,
|
|
115
|
-
auth: {
|
|
116
|
-
admin: ADMIN,
|
|
117
|
-
cookieDomain: "example.eu",
|
|
118
|
-
unsafeSkipOriginCheck: true,
|
|
119
|
-
},
|
|
120
|
-
});
|
|
121
|
-
expect(handle).toBeDefined();
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
describe("runDevApp — trustedProxyHops env fallback (KUMIKO_TRUSTED_PROXY_HOPS)", () => {
|
|
126
|
-
const clientIpRoute: AnonymousExtraRoute = {
|
|
127
|
-
method: "GET",
|
|
128
|
-
path: "/client-ip-probe",
|
|
129
|
-
entry: "anonymous",
|
|
130
|
-
handler: async (c, deps) => c.json({ clientIp: deps.clientIp }),
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
test("no trustedProxyHops option, env var set → env value reaches the server's client-ip resolver", async () => {
|
|
134
|
-
handle = await runDevApp({
|
|
135
|
-
features: [validFeature()],
|
|
136
|
-
port: 0,
|
|
137
|
-
extraRoutes: [clientIpRoute],
|
|
138
|
-
envSource: { ...process.env, KUMIKO_TRUSTED_PROXY_HOPS: "1" },
|
|
139
|
-
});
|
|
140
|
-
const res = await handle.fetch(
|
|
141
|
-
new Request("http://test/client-ip-probe", {
|
|
142
|
-
headers: { "x-forwarded-for": "203.0.113.10" },
|
|
143
|
-
}),
|
|
144
|
-
);
|
|
145
|
-
expect(await res.json()).toEqual({ clientIp: "203.0.113.10" });
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
test("explicit trustedProxyHops option wins over the env var", async () => {
|
|
149
|
-
handle = await runDevApp({
|
|
150
|
-
features: [validFeature()],
|
|
151
|
-
port: 0,
|
|
152
|
-
trustedProxyHops: 0,
|
|
153
|
-
extraRoutes: [clientIpRoute],
|
|
154
|
-
envSource: { ...process.env, KUMIKO_TRUSTED_PROXY_HOPS: "1" },
|
|
155
|
-
});
|
|
156
|
-
const res = await handle.fetch(
|
|
157
|
-
new Request("http://test/client-ip-probe", {
|
|
158
|
-
headers: { "x-forwarded-for": "203.0.113.10" },
|
|
159
|
-
}),
|
|
160
|
-
);
|
|
161
|
-
expect(await res.json()).toEqual({ clientIp: "unknown" });
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
describe("runDevApp — extraContext merge order: app values win over boot defaults (707/1)", () => {
|
|
166
|
-
test("a caller-supplied extraContext.templateResolver wins over the auto-wired boot default", async () => {
|
|
167
|
-
// runDevApp's extraContext factory does `{ ...boot, ...base }` — boot's
|
|
168
|
-
// auto-wired templateResolver must lose to a caller-supplied override, exactly
|
|
169
|
-
// the "app values win" parity claim this PR made against runProdApp. Every
|
|
170
|
-
// OTHER test in this file only checks that runDevApp boots without
|
|
171
|
-
// throwing; none dispatch a request that actually reads the merged value.
|
|
172
|
-
const sentinel: TemplateResolverApi = {
|
|
173
|
-
findExact: async () => ({
|
|
174
|
-
id: "sentinel",
|
|
175
|
-
version: 1,
|
|
176
|
-
tenantId: TestUsers.systemAdmin.tenantId,
|
|
177
|
-
slug: "sentinel",
|
|
178
|
-
kind: TEXT_BLOCK_KIND,
|
|
179
|
-
locale: "en",
|
|
180
|
-
title: "from caller extraContext",
|
|
181
|
-
folder: null,
|
|
182
|
-
content: "",
|
|
183
|
-
contentFormat: "markdown",
|
|
184
|
-
variableSchema: {},
|
|
185
|
-
linkedResources: {},
|
|
186
|
-
scope: "system",
|
|
187
|
-
parentTemplateId: null,
|
|
188
|
-
status: "active",
|
|
189
|
-
updatedAt: new Date(0),
|
|
190
|
-
}),
|
|
191
|
-
resolveTemplate: async () => {
|
|
192
|
-
throw new Error("not used in this test");
|
|
193
|
-
},
|
|
194
|
-
};
|
|
195
|
-
const readBlockQuery = defineQueryHandler({
|
|
196
|
-
name: "read-block",
|
|
197
|
-
schema: z.object({}),
|
|
198
|
-
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
199
|
-
handler: async (_query, ctx) => {
|
|
200
|
-
const api = requireTemplateResolver(ctx, "textcheck:query:read-block");
|
|
201
|
-
return api.findExact({
|
|
202
|
-
tenantId: TestUsers.systemAdmin.tenantId,
|
|
203
|
-
slug: "x",
|
|
204
|
-
kind: TEXT_BLOCK_KIND,
|
|
205
|
-
locale: "en",
|
|
206
|
-
});
|
|
207
|
-
},
|
|
208
|
-
});
|
|
209
|
-
const textcheckFeature = defineFeature("textcheck", (r) => {
|
|
210
|
-
r.queryHandler(readBlockQuery);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
handle = await runDevApp({
|
|
214
|
-
features: [validFeature(), textcheckFeature],
|
|
215
|
-
port: 0,
|
|
216
|
-
extraContext: { templateResolver: sentinel },
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
const res = await handle.stack.http.queryOk<{ title: string } | null>(
|
|
220
|
-
"textcheck:query:read-block",
|
|
221
|
-
{},
|
|
222
|
-
TestUsers.systemAdmin,
|
|
223
|
-
);
|
|
224
|
-
|
|
225
|
-
expect(res?.title).toBe("from caller extraContext");
|
|
226
|
-
});
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
// runDevApp had no equivalent of runProdApp's session boot gate (#1262/#1275):
|
|
230
|
-
// a forgotten sessions mount left session-list silently empty instead of
|
|
231
|
-
// saying anything (#2027). Unlike prod, dev warns rather than aborts —
|
|
232
|
-
// mirrors assertPiiBootInvariants' dev/prod split.
|
|
233
|
-
describe("runDevApp — session boot gate (#2027)", () => {
|
|
234
|
-
const ADMIN = {
|
|
235
|
-
email: "session-gate-dev@example.eu",
|
|
236
|
-
password: "test-pw-strong-1234",
|
|
237
|
-
displayName: "Admin",
|
|
238
|
-
emailVerified: true,
|
|
239
|
-
memberships: [
|
|
240
|
-
{
|
|
241
|
-
tenantId: "00000000-0000-4000-8000-000000000002" as TenantId,
|
|
242
|
-
tenantKey: "session-gate-dev",
|
|
243
|
-
tenantName: "Session Gate Dev",
|
|
244
|
-
roles: ["Admin"],
|
|
245
|
-
},
|
|
246
|
-
],
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
test("auth mounted, sessions feature missing → warns but still boots (dev doesn't abort)", async () => {
|
|
250
|
-
const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
251
|
-
try {
|
|
252
|
-
handle = await runDevApp({
|
|
253
|
-
features: [validFeature()],
|
|
254
|
-
port: 0,
|
|
255
|
-
auth: { admin: ADMIN },
|
|
256
|
-
});
|
|
257
|
-
expect(handle).toBeDefined();
|
|
258
|
-
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("sessionStore"));
|
|
259
|
-
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("[runDevApp]"));
|
|
260
|
-
} finally {
|
|
261
|
-
warnSpy.mockRestore();
|
|
262
|
-
}
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
test("auth mounted, sessions feature mounted → login actually creates a session (sid on the JWT)", async () => {
|
|
266
|
-
// The bug this issue reports: a mounted sessions feature whose sessionCreator
|
|
267
|
-
// never reached the auth config, so login never got a `sid` and
|
|
268
|
-
// store_user_sessions never got a row — session-list stayed empty with no
|
|
269
|
-
// error anywhere. A boot that doesn't throw proves nothing about that; only
|
|
270
|
-
// a real login + decoded JWT does.
|
|
271
|
-
handle = await runDevApp({
|
|
272
|
-
features: [
|
|
273
|
-
authFoundationFeature,
|
|
274
|
-
createPersonalAccessTokensFeature({ scopes: {} }),
|
|
275
|
-
createSessionsFeature(),
|
|
276
|
-
],
|
|
277
|
-
port: 0,
|
|
278
|
-
auth: { admin: ADMIN },
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
const login = await handle.stack.http.raw("POST", "/api/auth/login", {
|
|
282
|
-
email: ADMIN.email,
|
|
283
|
-
password: ADMIN.password,
|
|
284
|
-
});
|
|
285
|
-
expect(login.status).toBe(200);
|
|
286
|
-
const body = (await login.json()) as { token?: string };
|
|
287
|
-
expect(body.token).toBeTypeOf("string");
|
|
288
|
-
expect(typeof jose.decodeJwt(body.token!).jti).toBe("string");
|
|
289
|
-
});
|
|
290
|
-
});
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
|
|
5
|
-
// Mirrors infra/guards/runtime-isolation-classify.ts `classifyByDirective`
|
|
6
|
-
// as of kumiko-framework#2337 (first 600 bytes, first 8 lines,
|
|
7
|
-
// `// @runtime <kind>`) — guards against a reformat of compose-stacks.ts
|
|
8
|
-
// pushing the directive out of that window. Doesn't catch infra changing
|
|
9
|
-
// the window/regex itself; that lives in the infra repo's own tests.
|
|
10
|
-
const ALL_RUNTIMES = new Set(["runtime", "client", "dev", "tooling", "test"]);
|
|
11
|
-
|
|
12
|
-
function classifyByDirective(filePath: string): string | null {
|
|
13
|
-
const head = readFileSync(filePath, "utf8").slice(0, 600);
|
|
14
|
-
for (const line of head.split("\n").slice(0, 8)) {
|
|
15
|
-
const match = line.match(/\/\/\s*@runtime\s+(\w+)/);
|
|
16
|
-
if (match && ALL_RUNTIMES.has(match[1] ?? "")) return match[1] ?? null;
|
|
17
|
-
}
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
describe("compose-stacks.ts runtime directive", () => {
|
|
22
|
-
it("carries a @runtime runtime directive the guard's classifyByDirective will find", () => {
|
|
23
|
-
const composeStacksPath = fileURLToPath(new URL("../compose-stacks.ts", import.meta.url));
|
|
24
|
-
expect(classifyByDirective(composeStacksPath)).toBe("runtime");
|
|
25
|
-
});
|
|
26
|
-
});
|