@cosmicdrift/kumiko-server-runtime 0.329.0 → 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.
Files changed (90) hide show
  1. package/README.md +15 -0
  2. package/dist/boot/apply-boot-seeds.d.ts +8 -0
  3. package/{src/boot/apply-boot-seeds.ts → dist/boot/apply-boot-seeds.js} +2 -10
  4. package/dist/boot/boot-crypto.d.ts +9 -0
  5. package/dist/boot/boot-crypto.js +39 -0
  6. package/dist/boot/job-run-logger.d.ts +19 -0
  7. package/dist/boot/job-run-logger.js +44 -0
  8. package/dist/boot/pii-event-backfill-on-boot.d.ts +13 -0
  9. package/dist/boot/pii-event-backfill-on-boot.js +25 -0
  10. package/dist/build-prod-bundle.d.ts +103 -0
  11. package/dist/build-prod-bundle.js +751 -0
  12. package/dist/bun-serve-options.d.ts +32 -0
  13. package/dist/bun-serve-options.js +49 -0
  14. package/dist/compose-features.d.ts +31 -0
  15. package/dist/compose-features.js +124 -0
  16. package/dist/extra-routes-deps.d.ts +30 -0
  17. package/dist/extra-routes-deps.js +4 -0
  18. package/dist/index.d.ts +6 -0
  19. package/dist/index.js +7 -0
  20. package/dist/pii-boot-gate.d.ts +13 -0
  21. package/dist/pii-boot-gate.js +46 -0
  22. package/dist/renderer-web-fonts.d.ts +9 -0
  23. package/{src/renderer-web-fonts.ts → dist/renderer-web-fonts.js} +14 -18
  24. package/dist/resolve-tailwind-cli.d.ts +14 -0
  25. package/dist/resolve-tailwind-cli.js +36 -0
  26. package/dist/run-prod-app-boot-context.d.ts +43 -0
  27. package/dist/run-prod-app-boot-context.js +198 -0
  28. package/dist/run-prod-app-static-files.d.ts +32 -0
  29. package/dist/run-prod-app-static-files.js +280 -0
  30. package/dist/run-prod-app.d.ts +471 -0
  31. package/dist/run-prod-app.js +741 -0
  32. package/dist/run-worker-app.d.ts +92 -0
  33. package/dist/run-worker-app.js +220 -0
  34. package/dist/security-headers.d.ts +20 -0
  35. package/dist/security-headers.js +49 -0
  36. package/dist/session-boot-gate.d.ts +11 -0
  37. package/dist/session-boot-gate.js +22 -0
  38. package/dist/session-wiring.d.ts +11 -0
  39. package/{src/session-wiring.ts → dist/session-wiring.js} +2 -6
  40. package/dist/try-hono-first.d.ts +24 -0
  41. package/{src/try-hono-first.ts → dist/try-hono-first.js} +8 -36
  42. package/package.json +102 -36
  43. package/src/changes.json +6 -0
  44. package/src/__tests__/boot-extra-context.test.ts +0 -163
  45. package/src/__tests__/boot-probe-fixture.ts +0 -64
  46. package/src/__tests__/build-prod-bundle.inprocess.test.ts +0 -233
  47. package/src/__tests__/build-prod-bundle.test.ts +0 -724
  48. package/src/__tests__/cache-headers.test.ts +0 -83
  49. package/src/__tests__/compose-features-mfa-wiring.integration.test.ts +0 -308
  50. package/src/__tests__/compose-features-wiring.integration.test.ts +0 -609
  51. package/src/__tests__/compose-features.test.ts +0 -206
  52. package/src/__tests__/config-seed-boot.integration.test.ts +0 -158
  53. package/src/__tests__/pii-boot-gate.test.ts +0 -112
  54. package/src/__tests__/renderer-web-css-relocation.integration.test.ts +0 -85
  55. package/src/__tests__/renderer-web-shell-sentinel.test.ts +0 -35
  56. package/src/__tests__/renderer-web-theme-plugin.integration.test.ts +0 -77
  57. package/src/__tests__/require-env.test.ts +0 -34
  58. package/src/__tests__/resolve-auth-mail.test.ts +0 -144
  59. package/src/__tests__/resolve-tailwind-cli.test.ts +0 -81
  60. package/src/__tests__/run-prod-app-dry-run.test.ts +0 -135
  61. package/src/__tests__/run-prod-app-env-source.test.ts +0 -118
  62. package/src/__tests__/run-prod-app-kms-slots.test.ts +0 -108
  63. package/src/__tests__/run-prod-app-spec.test.ts +0 -167
  64. package/src/__tests__/run-prod-app-static-files.test.ts +0 -298
  65. package/src/__tests__/run-prod-app.integration.test.ts +0 -1525
  66. package/src/__tests__/run-worker-app-boot.test.ts +0 -183
  67. package/src/__tests__/run-worker-app.integration.test.ts +0 -218
  68. package/src/__tests__/security-headers.test.ts +0 -88
  69. package/src/__tests__/session-boot-gate.test.ts +0 -70
  70. package/src/__tests__/session-wiring.test.ts +0 -16
  71. package/src/__tests__/try-hono-first.test.ts +0 -108
  72. package/src/__tests__/websocket-route.integration.test.ts +0 -695
  73. package/src/boot/__tests__/job-run-logger.integration.test.ts +0 -262
  74. package/src/boot/__tests__/job-run-logger.test.ts +0 -26
  75. package/src/boot/boot-crypto.ts +0 -67
  76. package/src/boot/job-run-logger.ts +0 -63
  77. package/src/boot/pii-event-backfill-on-boot.ts +0 -43
  78. package/src/build-prod-bundle.ts +0 -996
  79. package/src/bun-serve-options.ts +0 -110
  80. package/src/compose-features.ts +0 -197
  81. package/src/extra-routes-deps.ts +0 -38
  82. package/src/index.ts +0 -27
  83. package/src/pii-boot-gate.ts +0 -80
  84. package/src/resolve-tailwind-cli.ts +0 -45
  85. package/src/run-prod-app-boot-context.ts +0 -319
  86. package/src/run-prod-app-static-files.ts +0 -359
  87. package/src/run-prod-app.ts +0 -1420
  88. package/src/run-worker-app.ts +0 -366
  89. package/src/security-headers.ts +0 -67
  90. package/src/session-boot-gate.ts +0 -34
@@ -1,34 +0,0 @@
1
- // requireEnv — the missing-var error message must match the actual caller.
2
- // Regression (728/2): the advice text was hardcoded to prod/container
3
- // wording ("Coolify secrets") regardless of the `context` param, so a
4
- // runDevApp caller got misleading production-deploy instructions for a
5
- // missing local .env var.
6
-
7
- import { describe, expect, test } from "bun:test";
8
- import { requireEnv as requireEnvFromPackageExport } from "@cosmicdrift/kumiko-server-runtime";
9
- import { requireEnv } from "../run-prod-app";
10
-
11
- describe("requireEnv", () => {
12
- test("is part of the package's public export, not just the internal run-prod-app module", () => {
13
- expect(requireEnvFromPackageExport).toBe(requireEnv);
14
- });
15
-
16
- test("default context (runProdApp) gives container/production advice", () => {
17
- expect(() => requireEnv("MISSING_VAR", {})).toThrow(/container env.*Coolify secrets/);
18
- });
19
-
20
- test("runDevApp context gives local .env advice, not production advice", () => {
21
- expect(() => requireEnv("MISSING_VAR", {}, "runDevApp")).toThrow(
22
- /\.env \/ shell before running the dev server/,
23
- );
24
- });
25
-
26
- test("runDevApp context error does NOT mention Coolify", () => {
27
- try {
28
- requireEnv("MISSING_VAR", {}, "runDevApp");
29
- throw new Error("expected requireEnv to throw");
30
- } catch (err) {
31
- expect(String(err)).not.toContain("Coolify");
32
- }
33
- });
34
- });
@@ -1,144 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { type RunProdAppAuthOptions, resolveAuthMail } from "../run-prod-app";
3
-
4
- // Pins the auth.mail convenience (resolveAuthMail): one mail block expands
5
- // into the four explicit flow setups built from DEFAULT_AUTH_PATHS, the
6
- // null-transport guard (no SMTP_HOST → flows stay unwired), and explicit
7
- // per-flow setups winning over the mail default.
8
-
9
- const admin: RunProdAppAuthOptions["admin"] = {
10
- email: "admin@example.com",
11
- password: "pw-long-enough",
12
- displayName: "Admin",
13
- memberships: [],
14
- };
15
-
16
- const withMail: RunProdAppAuthOptions = {
17
- admin,
18
- mail: { baseUrl: "https://app.example.com", appName: "Test" },
19
- };
20
-
21
- describe("resolveAuthMail", () => {
22
- test("no mail block → auth returned unchanged", () => {
23
- const noMail: RunProdAppAuthOptions = { admin };
24
- const out = resolveAuthMail(noMail, "secret", { SMTP_HOST: "localhost" });
25
- expect(out.passwordReset).toBeUndefined();
26
- expect(out.signup).toBeUndefined();
27
- });
28
-
29
- test("mail + SMTP_HOST → all four flows wired from DEFAULT_AUTH_PATHS", () => {
30
- const out = resolveAuthMail(withMail, "secret", { SMTP_HOST: "localhost" });
31
- // All four flows carry appUrl as feature options — each handler mails via
32
- // delivery (ctx.notify); no callback wiring remains.
33
- expect(out.passwordReset?.appUrl).toBe("https://app.example.com/reset-password");
34
- expect(out.emailVerification?.appUrl).toBe("https://app.example.com/verify-email");
35
- expect(out.signup?.appUrl).toBe("https://app.example.com/signup/complete");
36
- expect(out.invite?.appUrl).toBe("https://app.example.com/invite/accept");
37
- expect(out.passwordReset?.hmacSecret).toBe("secret");
38
- });
39
-
40
- test("mail but NO SMTP_HOST → null-transport guard, flows stay unwired", () => {
41
- const out = resolveAuthMail(withMail, "secret", {});
42
- expect(out.passwordReset).toBeUndefined();
43
- expect(out.signup).toBeUndefined();
44
- });
45
-
46
- test("explicit per-flow setup wins over the mail default", () => {
47
- const explicit: RunProdAppAuthOptions = {
48
- ...withMail,
49
- passwordReset: {
50
- hmacSecret: "h",
51
- appUrl: "https://custom.example.com/pw",
52
- },
53
- };
54
- const out = resolveAuthMail(explicit, "secret", { SMTP_HOST: "localhost" });
55
- expect(out.passwordReset?.appUrl).toBe("https://custom.example.com/pw");
56
- // other flows still come from the mail default
57
- expect(out.signup?.appUrl).toBe("https://app.example.com/signup/complete");
58
- });
59
-
60
- test("paths override only affects the named path", () => {
61
- const pathsOverride: RunProdAppAuthOptions = {
62
- admin,
63
- mail: { baseUrl: "https://app.example.com", paths: { resetPassword: "/pw" } },
64
- };
65
- const out = resolveAuthMail(pathsOverride, "secret", { SMTP_HOST: "localhost" });
66
- expect(out.passwordReset?.appUrl).toBe("https://app.example.com/pw");
67
- expect(out.emailVerification?.appUrl).toBe("https://app.example.com/verify-email");
68
- });
69
-
70
- test("function path → locale-aware appUrl, still signed with the resolved hmacSecret", () => {
71
- const localeAware: RunProdAppAuthOptions = {
72
- admin,
73
- mail: {
74
- baseUrl: "https://app.example.com",
75
- paths: {
76
- resetPassword: (locale) => (locale === "de" ? "/de/reset-password" : "/reset-password"),
77
- },
78
- },
79
- };
80
- const out = resolveAuthMail(localeAware, "secret", { SMTP_HOST: "localhost" });
81
- const appUrl = out.passwordReset?.appUrl;
82
- if (typeof appUrl !== "function") throw new Error("expected a locale-aware appUrl function");
83
- expect(appUrl("de")).toBe("https://app.example.com/de/reset-password");
84
- expect(appUrl("en")).toBe("https://app.example.com/reset-password");
85
- expect(out.passwordReset?.hmacSecret).toBe("secret");
86
- });
87
-
88
- test("mail.hmacSecret overrides the call-site secret for passwordReset and emailVerification", () => {
89
- const dedicatedSecret: RunProdAppAuthOptions = {
90
- admin,
91
- mail: { baseUrl: "https://app.example.com", hmacSecret: "mail-only-secret" },
92
- };
93
- const out = resolveAuthMail(dedicatedSecret, "jwt-secret", { SMTP_HOST: "localhost" });
94
- expect(out.passwordReset?.hmacSecret).toBe("mail-only-secret");
95
- expect(out.emailVerification?.hmacSecret).toBe("mail-only-secret");
96
- });
97
-
98
- test("no mail.hmacSecret → falls back to the call-site secret", () => {
99
- const out = resolveAuthMail(withMail, "jwt-secret", { SMTP_HOST: "localhost" });
100
- expect(out.passwordReset?.hmacSecret).toBe("jwt-secret");
101
- });
102
-
103
- test("explicit passwordReset/emailVerification without hmacSecret is backfilled from mail.hmacSecret", () => {
104
- const explicitNoSecret: RunProdAppAuthOptions = {
105
- admin,
106
- mail: { baseUrl: "https://app.example.com", hmacSecret: "mail-only-secret" },
107
- passwordReset: { appUrl: "https://custom.example.com/pw" },
108
- emailVerification: { appUrl: "https://custom.example.com/verify" },
109
- };
110
- const out = resolveAuthMail(explicitNoSecret, "jwt-secret", { SMTP_HOST: "localhost" });
111
- expect(out.passwordReset?.appUrl).toBe("https://custom.example.com/pw");
112
- expect(out.passwordReset?.hmacSecret).toBe("mail-only-secret");
113
- expect(out.emailVerification?.hmacSecret).toBe("mail-only-secret");
114
- });
115
-
116
- test("explicit passwordReset with its own hmacSecret wins over mail.hmacSecret", () => {
117
- const explicitOwnSecret: RunProdAppAuthOptions = {
118
- admin,
119
- mail: { baseUrl: "https://app.example.com", hmacSecret: "mail-only-secret" },
120
- passwordReset: { appUrl: "https://custom.example.com/pw", hmacSecret: "app-own-secret" },
121
- };
122
- const out = resolveAuthMail(explicitOwnSecret, "jwt-secret", { SMTP_HOST: "localhost" });
123
- expect(out.passwordReset?.hmacSecret).toBe("app-own-secret");
124
- });
125
-
126
- test("explicit passwordReset without hmacSecret and no mail block falls back to the call-site secret", () => {
127
- const explicitNoMail: RunProdAppAuthOptions = {
128
- admin,
129
- passwordReset: { appUrl: "https://custom.example.com/pw" },
130
- };
131
- const out = resolveAuthMail(explicitNoMail, "jwt-secret", {});
132
- expect(out.passwordReset?.hmacSecret).toBe("jwt-secret");
133
- });
134
-
135
- test("explicit passwordReset without hmacSecret is still backfilled when SMTP_HOST is unset", () => {
136
- const explicitNoSmtp: RunProdAppAuthOptions = {
137
- admin,
138
- mail: { baseUrl: "https://app.example.com", hmacSecret: "mail-only-secret" },
139
- passwordReset: { appUrl: "https://custom.example.com/pw" },
140
- };
141
- const out = resolveAuthMail(explicitNoSmtp, "jwt-secret", {});
142
- expect(out.passwordReset?.hmacSecret).toBe("mail-only-secret");
143
- });
144
- });
@@ -1,81 +0,0 @@
1
- // Unit-Tests für resolveTailwindCli — die zwei Failure-Branches sind
2
- // genau das, was den dev-server-Crash bei flakigem Netz verhindert:
3
- // kein Bun → undefined, Package nicht installiert → undefined.
4
-
5
- import { describe, expect, test } from "bun:test";
6
- import { canResolveTailwindStylesheet, resolveTailwindCli } from "../resolve-tailwind-cli";
7
-
8
- describe("resolveTailwindCli", () => {
9
- test("ohne Bun-Resolver → undefined (silent skip)", () => {
10
- const out = resolveTailwindCli({ bun: undefined, cwd: "/somewhere" });
11
- expect(out).toBeUndefined();
12
- });
13
-
14
- test("Bun.resolveSync wirft → undefined", () => {
15
- const out = resolveTailwindCli({
16
- bun: {
17
- resolveSync: () => {
18
- throw new Error("module not found");
19
- },
20
- },
21
- cwd: "/somewhere",
22
- });
23
- expect(out).toBeUndefined();
24
- });
25
-
26
- test("Bun.resolveSync liefert package.json → absoluter Bin-Pfad", () => {
27
- const out = resolveTailwindCli({
28
- bun: {
29
- resolveSync: () => "/repo/node_modules/@tailwindcss/cli/package.json",
30
- },
31
- cwd: "/repo",
32
- });
33
- expect(out).toBe("/repo/node_modules/@tailwindcss/cli/dist/index.mjs");
34
- });
35
-
36
- test("Bun-Resolver wird mit korrektem id und cwd aufgerufen", () => {
37
- const calls: Array<{ id: string; from: string }> = [];
38
- resolveTailwindCli({
39
- bun: {
40
- resolveSync: (id, from) => {
41
- calls.push({ id, from });
42
- return "/x/node_modules/@tailwindcss/cli/package.json";
43
- },
44
- },
45
- cwd: "/some/working/dir",
46
- });
47
- expect(calls).toEqual([{ id: "@tailwindcss/cli/package.json", from: "/some/working/dir" }]);
48
- });
49
- });
50
-
51
- describe("canResolveTailwindStylesheet", () => {
52
- test("tailwindcss resolvable from entry dir → true", () => {
53
- const out = canResolveTailwindStylesheet("/repo/packages/app/src/styles.css", {
54
- bun: {
55
- resolveSync: (id, from) => {
56
- if (id === "tailwindcss" && from === "/repo/packages/app/src") {
57
- return "/repo/node_modules/tailwindcss/index.css";
58
- }
59
- throw new Error("not found");
60
- },
61
- },
62
- cwd: "/repo/packages/app",
63
- });
64
- expect(out).toBe(true);
65
- });
66
-
67
- test("tailwindcss not resolvable from entry dir → false", () => {
68
- const out = canResolveTailwindStylesheet(
69
- "/cache/@cosmicdrift/kumiko-renderer-web/src/styles.css",
70
- {
71
- bun: {
72
- resolveSync: () => {
73
- throw new Error("not found");
74
- },
75
- },
76
- cwd: "/tmp/minimal-fixture",
77
- },
78
- );
79
- expect(out).toBe(false);
80
- });
81
- });
@@ -1,135 +0,0 @@
1
- // Dry-run + bootErrorReporter paths — no DB/Redis. envSource avoids process.exit(0).
2
-
3
- import { describe, expect, test } from "bun:test";
4
- import { composeEnvSchema, KumikoBootError } from "@cosmicdrift/kumiko-framework/env";
5
- import * as z from "zod";
6
- import { runProdApp } from "../run-prod-app";
7
- import { makeProbeFeature, withClearedBootEnv } from "./boot-probe-fixture";
8
-
9
- const probeFeature = makeProbeFeature({
10
- name: "dry-run-probe",
11
- table: "dry_run_probe",
12
- extraSetup: (r) => {
13
- r.envSchema(z.object({ DRY_RUN_PROBE: z.string().optional().describe("probe var") }));
14
- },
15
- });
16
-
17
- describe("runProdApp dry-run / bootErrorReporter", () => {
18
- withClearedBootEnv();
19
-
20
- test("KUMIKO_DRY_RUN_ENV=human + envSource → render + dry-run handle (no exit)", async () => {
21
- const logs: string[] = [];
22
- const originalLog = console.log;
23
- console.log = (...args: unknown[]) => {
24
- logs.push(args.map(String).join(" "));
25
- };
26
- const envSchema = composeEnvSchema({ features: [probeFeature] });
27
- let handle: Awaited<ReturnType<typeof runProdApp>>;
28
- try {
29
- handle = await runProdApp({
30
- features: [probeFeature],
31
- envSchema,
32
- autoListen: false,
33
- migrations: false,
34
- envSource: { KUMIKO_DRY_RUN_ENV: "human" },
35
- });
36
- } finally {
37
- console.log = originalLog;
38
- }
39
-
40
- expect(logs.some((l) => l.includes("DRY_RUN_PROBE"))).toBe(true);
41
- const res = await handle!.fetch(new Request("http://test/"));
42
- expect(res.status).toBe(503);
43
- expect(await res.text()).toBe("dry-run");
44
- await handle!.stop();
45
- });
46
-
47
- test("KUMIKO_DRY_RUN_ENV=json → structured dry-run output", async () => {
48
- const logs: string[] = [];
49
- const originalLog = console.log;
50
- console.log = (...args: unknown[]) => {
51
- logs.push(args.map(String).join(" "));
52
- };
53
- const envSchema = composeEnvSchema({ features: [probeFeature] });
54
- try {
55
- const handle = await runProdApp({
56
- features: [probeFeature],
57
- envSchema,
58
- autoListen: false,
59
- migrations: false,
60
- envSource: { KUMIKO_DRY_RUN_ENV: "json" },
61
- });
62
- await handle.stop();
63
- } finally {
64
- console.log = originalLog;
65
- }
66
- const jsonLine = logs.find((l) => l.trimStart().startsWith("{"));
67
- if (!jsonLine) throw new Error(`No JSON line in logs: ${JSON.stringify(logs)}`);
68
- const parsed = JSON.parse(jsonLine) as {
69
- optional: Array<{ name: string; feature: string }>;
70
- };
71
- expect(parsed.optional).toContainEqual(
72
- expect.objectContaining({ name: "DRY_RUN_PROBE", feature: "dry-run-probe" }),
73
- );
74
- });
75
-
76
- test("unrecognized KUMIKO_DRY_RUN_ENV warns then hits envSchema parse", async () => {
77
- const warnings: string[] = [];
78
- const originalWarn = console.warn;
79
- console.warn = (...args: unknown[]) => {
80
- warnings.push(args.map(String).join(" "));
81
- };
82
- const envSchema = composeEnvSchema({
83
- features: [],
84
- extend: z.object({ MUST_HAVE: z.string().describe("required for test") }),
85
- });
86
- try {
87
- await expect(
88
- runProdApp({
89
- features: [probeFeature],
90
- envSchema,
91
- autoListen: false,
92
- migrations: false,
93
- envSource: { KUMIKO_DRY_RUN_ENV: "not-a-real-mode" },
94
- bootErrorReporter: (err) => {
95
- throw err;
96
- },
97
- }),
98
- ).rejects.toBeInstanceOf(KumikoBootError);
99
- } finally {
100
- console.warn = originalWarn;
101
- }
102
- expect(
103
- warnings.some(
104
- (w) => w.includes('KUMIKO_DRY_RUN_ENV="not-a-real-mode"') && w.includes("unrecognized"),
105
- ),
106
- ).toBe(true);
107
- });
108
-
109
- test("bootErrorReporter receives KumikoBootError instead of process.exit", async () => {
110
- const envSchema = composeEnvSchema({
111
- features: [],
112
- extend: z.object({ MUST_HAVE: z.string().describe("required for test") }),
113
- });
114
- let reported: KumikoBootError | undefined;
115
- await expect(
116
- runProdApp({
117
- features: [probeFeature],
118
- envSchema,
119
- autoListen: false,
120
- migrations: false,
121
- envSource: {
122
- // no MUST_HAVE → parseEnv throws KumikoBootError
123
- DATABASE_URL: "postgres://x",
124
- REDIS_URL: "redis://x",
125
- },
126
- bootErrorReporter: (err) => {
127
- reported = err;
128
- throw err;
129
- },
130
- }),
131
- ).rejects.toBeInstanceOf(KumikoBootError);
132
- expect(reported).toBeInstanceOf(KumikoBootError);
133
- expect(reported!.errors.some((e) => e.name === "MUST_HAVE")).toBe(true);
134
- });
135
- });
@@ -1,118 +0,0 @@
1
- // Regression: the boot-path must read the injected `envSource`, not the real
2
- // process.env. Boot-mode (KUMIKO_DRY_RUN_ENV=boot) validates wiring + builds
3
- // the registry, then tears down the lazy DB/Redis clients before any socket
4
- // opens — so this runs without a real Postgres/Redis (same as the CI boot
5
- // smoke). Before the fix, requireEnv/readEnv read process.env directly, so the
6
- // required-var test would throw "required env var DATABASE_URL is missing" and
7
- // the PORT test would bind the default instead of the injected port.
8
-
9
- import { describe, expect, test } from "bun:test";
10
- import { runProdApp } from "../run-prod-app";
11
- import { makeProbeFeature, withClearedBootEnv } from "./boot-probe-fixture";
12
-
13
- const probeFeature = makeProbeFeature({
14
- name: "env-source-probe",
15
- table: "env_source_probe",
16
- });
17
-
18
- const DUMMY_ENV = {
19
- KUMIKO_DRY_RUN_ENV: "boot",
20
- DATABASE_URL: "postgres://smoke:smoke@127.0.0.1:1/smoke",
21
- REDIS_URL: "redis://127.0.0.1:1",
22
- JWT_SECRET: "smokesmokesmokesmokesmokesmokesmokesmoke",
23
- } as const;
24
-
25
- describe("runProdApp boot-mode env-source", () => {
26
- withClearedBootEnv();
27
-
28
- test("boots from injected envSource even when process.env lacks the required vars", async () => {
29
- const logs: string[] = [];
30
- const originalLog = console.log;
31
- console.log = (...args: unknown[]) => {
32
- logs.push(args.map(String).join(" "));
33
- };
34
- let handle: Awaited<ReturnType<typeof runProdApp>>;
35
- try {
36
- handle = await runProdApp({
37
- features: [probeFeature],
38
- autoListen: false,
39
- migrations: false,
40
- // REDIS_URL points at an unreachable port — boot-mode must NOT
41
- // construct the (eager) Redis client, so this never tries to connect.
42
- envSource: { ...DUMMY_ENV },
43
- });
44
- } finally {
45
- console.log = originalLog;
46
- }
47
-
48
- // Boot-mode with an injected envSource returns an inert dry-run handle.
49
- expect(handle).toBeDefined();
50
- expect(typeof handle.stop).toBe("function");
51
- // The registry was built + validated before any connection was opened.
52
- expect(logs.some((line) => line.includes("boot validation OK"))).toBe(true);
53
- await handle.stop();
54
- });
55
-
56
- test("boot-mode constructs no eager Redis client — kein TCP-Connect auf REDIS_URL", async () => {
57
- // Kern-Garantie (224/2), als Netzwerk-Beweis statt Konstruktor-Spy:
58
- // REDIS_URL zeigt auf einen lokalen Listener — `new Redis(...)`
59
- // connectet eager, der Boot-Exit MUSS also vorher liegen, sonst
60
- // zählt der Listener eine Connection.
61
- let connections = 0;
62
- const listener = Bun.listen({
63
- hostname: "127.0.0.1",
64
- port: 0,
65
- socket: {
66
- open(socket) {
67
- connections += 1;
68
- socket.end();
69
- },
70
- data() {},
71
- },
72
- });
73
-
74
- const originalLog = console.log;
75
- console.log = () => {};
76
- try {
77
- const handle = await runProdApp({
78
- features: [probeFeature],
79
- autoListen: false,
80
- migrations: false,
81
- envSource: { ...DUMMY_ENV, REDIS_URL: `redis://127.0.0.1:${listener.port}` },
82
- });
83
- await handle.stop();
84
- } finally {
85
- console.log = originalLog;
86
- }
87
-
88
- // Ein eager Connect wäre bereits beim runProdApp-await passiert;
89
- // kleine Nachfrist für asynchrone Socket-Anläufe.
90
- await new Promise((resolve) => setTimeout(resolve, 150));
91
- listener.stop(true);
92
- expect(connections).toBe(0);
93
- });
94
-
95
- test("resolves PORT from envSource, not process.env", async () => {
96
- const logs: string[] = [];
97
- const originalLog = console.log;
98
- console.log = (...args: unknown[]) => {
99
- logs.push(args.map(String).join(" "));
100
- };
101
- try {
102
- const handle = await runProdApp({
103
- features: [probeFeature],
104
- autoListen: false,
105
- migrations: false,
106
- envSource: { ...DUMMY_ENV, PORT: "8123" },
107
- });
108
- await handle.stop();
109
- } finally {
110
- console.log = originalLog;
111
- }
112
-
113
- // The boot logs "booting Kumiko stack on port <port>" — pre-fix this read
114
- // process.env["PORT"] (deleted here) and would log the 3000 default.
115
- expect(logs.some((line) => line.includes("port 8123"))).toBe(true);
116
- expect(logs.some((line) => line.includes("port 3000"))).toBe(false);
117
- });
118
- });
@@ -1,108 +0,0 @@
1
- // A schema field marked `kms` is decrypted once at boot and the RESOLVED env
2
- // feeds the master-key probe: a ciphertext-only KUMIKO_SECRETS_MASTER_KEY_V1
3
- // boots, and without any key the probe still fails. Boot-mode runs without
4
- // Postgres/Redis (see run-prod-app-env-source.test.ts).
5
-
6
- import { afterEach, describe, expect, test } from "bun:test";
7
- import { createEntity, createTextField, defineFeature } from "@cosmicdrift/kumiko-framework/engine";
8
- import type { ComposedEnvSchema } from "@cosmicdrift/kumiko-framework/env";
9
- import * as z from "zod";
10
- import { runProdApp } from "../run-prod-app";
11
- import { withClearedBootEnv } from "./boot-probe-fixture";
12
-
13
- const MASTER_KEY = Buffer.alloc(32, 7).toString("base64");
14
-
15
- const encryptedFeature = defineFeature("kms-slots-probe", (r) => {
16
- r.entity(
17
- "note",
18
- createEntity({
19
- table: "kms_slots_probe_note",
20
- fields: {
21
- body: createTextField({ personal: false, reason: "test_fixture", encrypted: true }),
22
- },
23
- }),
24
- );
25
- });
26
-
27
- const envSchema: ComposedEnvSchema = {
28
- schema: z.object({
29
- KUMIKO_SECRETS_MASTER_KEY_V1: z.string().meta({ kumiko: { kms: true } }),
30
- }),
31
- sources: {},
32
- };
33
-
34
- const BASE_ENV = {
35
- KUMIKO_DRY_RUN_ENV: "boot",
36
- DATABASE_URL: "postgres://smoke:smoke@127.0.0.1:1/smoke",
37
- REDIS_URL: "redis://127.0.0.1:1",
38
- JWT_SECRET: "smokesmokesmokesmokesmokesmokesmokesmoke",
39
- } as const;
40
-
41
- const originalFetch = globalThis.fetch;
42
- const originalLog = console.log;
43
- const originalInfo = console.info;
44
-
45
- function bootWith(env: Record<string, string>): ReturnType<typeof runProdApp> {
46
- return runProdApp({
47
- features: [encryptedFeature],
48
- envSchema,
49
- autoListen: false,
50
- migrations: false,
51
- envSource: { ...BASE_ENV, ...env },
52
- });
53
- }
54
-
55
- describe("runProdApp kms slots", () => {
56
- withClearedBootEnv();
57
- afterEach(() => {
58
- globalThis.fetch = originalFetch;
59
- console.log = originalLog;
60
- console.info = originalInfo;
61
- });
62
-
63
- test("boots with a master key that exists only as Key Manager ciphertext", async () => {
64
- const decrypted: string[] = [];
65
- globalThis.fetch = (async (_url: string | URL, init?: RequestInit) => {
66
- decrypted.push(JSON.parse(String(init?.body)).ciphertext);
67
- return new Response(JSON.stringify({ plaintext: MASTER_KEY }), { status: 200 });
68
- }) as unknown as typeof globalThis.fetch;
69
- console.log = () => {};
70
- console.info = () => {};
71
-
72
- const handle = await bootWith({
73
- KUMIKO_SECRETS_MASTER_KEY_V1_CIPHERTEXT: "Y2lwaGVy",
74
- PLATFORM_KEK_KMS_KEY_ID: "key-1",
75
- PLATFORM_KEK_KMS_TOKEN: "token",
76
- });
77
-
78
- await handle.stop();
79
- expect(decrypted).toEqual(["Y2lwaGVy"]);
80
- });
81
-
82
- test("a plaintext master key boots without any Key Manager request", async () => {
83
- let requests = 0;
84
- globalThis.fetch = (async () => {
85
- requests += 1;
86
- throw new Error("must not be called");
87
- }) as unknown as typeof globalThis.fetch;
88
- console.log = () => {};
89
- console.info = () => {};
90
-
91
- const handle = await bootWith({
92
- KUMIKO_SECRETS_MASTER_KEY_V1: MASTER_KEY,
93
- KUMIKO_SECRETS_MASTER_KEY_V1_CIPHERTEXT: "Y2lwaGVy",
94
- });
95
-
96
- await handle.stop();
97
- expect(requests).toBe(0);
98
- });
99
-
100
- test("without plaintext or ciphertext the boot probe still rejects", async () => {
101
- console.log = () => {};
102
- console.info = () => {};
103
-
104
- await expect(bootWith({ KUMIKO_SECRETS_MASTER_KEY_V1: "" })).rejects.toThrow(
105
- /no usable master key/,
106
- );
107
- });
108
- });