@cosmicdrift/kumiko-server-runtime 0.292.0 → 0.294.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.292.0",
3
+ "version": "0.294.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>",
@@ -80,9 +80,9 @@
80
80
  }
81
81
  },
82
82
  "dependencies": {
83
- "@cosmicdrift/kumiko-bundled-features": "0.292.0",
84
- "@cosmicdrift/kumiko-framework": "0.292.0",
85
- "@cosmicdrift/kumiko-headless": "0.292.0",
83
+ "@cosmicdrift/kumiko-bundled-features": "0.294.0",
84
+ "@cosmicdrift/kumiko-framework": "0.294.0",
85
+ "@cosmicdrift/kumiko-headless": "0.294.0",
86
86
  "temporal-polyfill": "^0.3.2"
87
87
  },
88
88
  "publishConfig": {
@@ -0,0 +1,108 @@
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 { 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
+ });
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { describe, expect, spyOn, test } from "bun:test";
7
7
  import type { KmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
8
+ import { createEntity, createTextField, defineFeature } from "@cosmicdrift/kumiko-framework/engine";
8
9
  import { runWorkerApp } from "../run-worker-app";
9
10
  import { makeProbeFeature, withClearedBootEnv } from "./boot-probe-fixture";
10
11
 
@@ -132,4 +133,51 @@ describe("runWorkerApp boot-mode", () => {
132
133
  console.log = originalLog;
133
134
  }
134
135
  });
136
+
137
+ test("kmsSlots: a master key that exists only as ciphertext reaches the boot probe", async () => {
138
+ const encryptedFeature = defineFeature("worker-kms-probe", (r) => {
139
+ r.entity(
140
+ "note",
141
+ createEntity({
142
+ table: "worker_kms_probe_note",
143
+ fields: {
144
+ body: createTextField({ personal: false, reason: "test_fixture", encrypted: true }),
145
+ },
146
+ }),
147
+ );
148
+ });
149
+ const originalFetch = globalThis.fetch;
150
+ const originalLog = console.log;
151
+ const originalInfo = console.info;
152
+ globalThis.fetch = (async () =>
153
+ new Response(JSON.stringify({ plaintext: Buffer.alloc(32, 7).toString("base64") }), {
154
+ status: 200,
155
+ })) as unknown as typeof globalThis.fetch;
156
+ console.log = () => {};
157
+ console.info = () => {};
158
+ const env = {
159
+ ...DUMMY_ENV,
160
+ KUMIKO_DRY_RUN_ENV: "boot",
161
+ KUMIKO_SECRETS_MASTER_KEY_V1_CIPHERTEXT: "Y2lwaGVy",
162
+ PLATFORM_KEK_KMS_KEY_ID: "key-1",
163
+ PLATFORM_KEK_KMS_TOKEN: "token",
164
+ };
165
+ try {
166
+ await expect(
167
+ runWorkerApp({ features: [encryptedFeature], migrations: false, envSource: env }),
168
+ ).rejects.toThrow(/no usable master key/);
169
+
170
+ const handle = await runWorkerApp({
171
+ features: [encryptedFeature],
172
+ migrations: false,
173
+ envSource: env,
174
+ kmsSlots: ["KUMIKO_SECRETS_MASTER_KEY_V1"],
175
+ });
176
+ await handle.stop();
177
+ } finally {
178
+ globalThis.fetch = originalFetch;
179
+ console.log = originalLog;
180
+ console.info = originalInfo;
181
+ }
182
+ });
135
183
  });
@@ -22,7 +22,6 @@ import {
22
22
  createTextField,
23
23
  defineFeature,
24
24
  } from "@cosmicdrift/kumiko-framework/engine";
25
- import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
26
25
  import { createDispatcher } from "@cosmicdrift/kumiko-framework/pipeline";
27
26
  import {
28
27
  createTestDb,
@@ -56,7 +55,6 @@ beforeAll(async () => {
56
55
  testRedis = await createTestRedis();
57
56
  db = testDb.db;
58
57
  await unsafePushTables(db, { jobRunsTable, jobRunLogsTable });
59
- await createEventsTable(db);
60
58
  });
61
59
 
62
60
  afterAll(async () => {
@@ -81,6 +81,7 @@ import {
81
81
  configureBlindIndexKey,
82
82
  configurePiiSubjectKms,
83
83
  type KmsAdapter,
84
+ resolvePlatformKeks,
84
85
  } from "@cosmicdrift/kumiko-framework/crypto";
85
86
  import {
86
87
  configureEntityFieldEncryption,
@@ -108,6 +109,7 @@ import {
108
109
  import {
109
110
  type ComposedEnvSchema,
110
111
  KumikoBootError,
112
+ kmsSlotsOf,
111
113
  parseEnv,
112
114
  } from "@cosmicdrift/kumiko-framework/env";
113
115
  import { type DryRunMode, renderDryRun } from "@cosmicdrift/kumiko-framework/env/dry-run";
@@ -706,8 +708,8 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
706
708
  // at parse-time before the polyfill loads. Plain strings + .regex /
707
709
  // .min / .email / .url cover every env-var shape we've actually
708
710
  // needed in 9.1's audit (37 references, 25 distinct vars).
709
- const envSource = options.envSource ?? process.env;
710
- const runMode = parseRunMode(envSource["KUMIKO_DRY_RUN_ENV"]);
711
+ const rawEnvSource = options.envSource ?? process.env;
712
+ const runMode = parseRunMode(rawEnvSource["KUMIKO_DRY_RUN_ENV"]);
711
713
  if (options.envSchema) {
712
714
  if (isRenderMode(runMode)) {
713
715
  // biome-ignore lint/suspicious/noConsole: dry-run output IS the deliverable
@@ -729,7 +731,7 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
729
731
  // a real env-check (all required vars present + schema-valid) before
730
732
  // it asserts feature-wiring works.
731
733
  try {
732
- parseEnv(options.envSchema.schema, envSource, {
734
+ parseEnv(options.envSchema.schema, rawEnvSource, {
733
735
  sources: options.envSchema.sources,
734
736
  ...(options.pulumiPrefix ? { pulumiPrefix: options.pulumiPrefix } : {}),
735
737
  });
@@ -742,6 +744,15 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
742
744
  }
743
745
  }
744
746
 
747
+ // Slots the schema marks `kms` are decrypted once here so every consumer
748
+ // below (boot probe, master-key keyring, config) sees the same resolved env.
749
+ const envSource: Record<string, string | undefined> = options.envSchema
750
+ ? await resolvePlatformKeks(rawEnvSource, {
751
+ slots: kmsSlotsOf(options.envSchema.schema),
752
+ logPrefix: "[runProdApp]",
753
+ })
754
+ : rawEnvSource;
755
+
745
756
  // 1. Polyfill before anything else — feature code references Temporal.
746
757
  const { ensureTemporalPolyfill } = await import("@cosmicdrift/kumiko-framework/time");
747
758
  await ensureTemporalPolyfill();
@@ -818,7 +829,7 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
818
829
  ...(composeAuthOptions && { authOptions: composeAuthOptions }),
819
830
  });
820
831
 
821
- validateBoot(features, options.validateBootOptions);
832
+ validateBoot(features, { env: envSource, ...options.validateBootOptions });
822
833
  warnIfNonUtcServerTimeZone();
823
834
  validateAppCustomScreenWriteQns(process.cwd(), collectWriteHandlerQns(features));
824
835
  assertPiiBootInvariants(features, {
@@ -20,6 +20,7 @@ import {
20
20
  configureBlindIndexKey,
21
21
  configurePiiSubjectKms,
22
22
  type KmsAdapter,
23
+ resolvePlatformKeks,
23
24
  } from "@cosmicdrift/kumiko-framework/crypto";
24
25
  import {
25
26
  configureEntityFieldEncryption,
@@ -136,6 +137,10 @@ export type RunWorkerAppOptions = {
136
137
  /** Override `process.env` for env-validation (see
137
138
  * RunProdAppOptions["envSource"]). */
138
139
  readonly envSource?: Record<string, string | undefined>;
140
+ /** Env names to decrypt from their `_CIPHERTEXT` twin at boot, typically
141
+ * `kmsSlotsOf(composedEnv.schema)`. The worker parses no env schema, so it
142
+ * cannot derive them; without this it reads the env as is. */
143
+ readonly kmsSlots?: readonly string[];
139
144
  readonly observability?: ObservabilityProvider;
140
145
  readonly observabilityOptions?: ObservabilityOptions;
141
146
  };
@@ -157,7 +162,13 @@ function makeBootModeHandle(): WorkerAppHandle {
157
162
  }
158
163
 
159
164
  export async function runWorkerApp(options: RunWorkerAppOptions): Promise<WorkerAppHandle> {
160
- const envSource = options.envSource ?? process.env;
165
+ const rawEnvSource = options.envSource ?? process.env;
166
+ const envSource: Record<string, string | undefined> = options.kmsSlots
167
+ ? await resolvePlatformKeks(rawEnvSource, {
168
+ slots: options.kmsSlots,
169
+ logPrefix: "[runWorkerApp]",
170
+ })
171
+ : rawEnvSource;
161
172
 
162
173
  // 1. Polyfill before anything else — exactly the bug fw#1725 reports:
163
174
  // without it, every job in the worker fails with "Temporal is not
@@ -180,7 +191,7 @@ export async function runWorkerApp(options: RunWorkerAppOptions): Promise<Worker
180
191
  const features = composeFeatures(options.features, {
181
192
  includeBundled: !!options.includeBundled,
182
193
  });
183
- validateBoot(features, options.validateBootOptions);
194
+ validateBoot(features, { env: envSource, ...options.validateBootOptions });
184
195
  warnIfNonUtcServerTimeZone();
185
196
  assertPiiBootInvariants(features, {
186
197
  kms: options.kms,