@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.
|
|
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.
|
|
84
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
85
|
-
"@cosmicdrift/kumiko-headless": "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 () => {
|
package/src/run-prod-app.ts
CHANGED
|
@@ -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
|
|
710
|
-
const runMode = parseRunMode(
|
|
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,
|
|
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, {
|
package/src/run-worker-app.ts
CHANGED
|
@@ -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
|
|
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,
|