@cosmicdrift/kumiko-dev-server 0.116.1 → 0.118.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-dev-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.118.0",
|
|
4
4
|
"description": "Development server bootstrap for Kumiko apps. Bundles the client, mints dev-JWTs, injects the resolved AppSchema, and seeds an admin. Not for production.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"kumiko-schema-check": "./bin/kumiko-schema-check.ts"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@cosmicdrift/kumiko-bundled-features": "0.
|
|
54
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
53
|
+
"@cosmicdrift/kumiko-bundled-features": "0.118.0",
|
|
54
|
+
"@cosmicdrift/kumiko-framework": "0.118.0",
|
|
55
55
|
"ts-morph": "^28.0.0"
|
|
56
56
|
},
|
|
57
57
|
"publishConfig": {
|
|
@@ -14,6 +14,7 @@ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
|
14
14
|
import { tmpdir } from "node:os";
|
|
15
15
|
import { dirname, join } from "node:path";
|
|
16
16
|
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
17
|
+
import { InMemoryKmsAdapter, type KmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
|
|
17
18
|
import { createDbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
18
19
|
import {
|
|
19
20
|
createBooleanField,
|
|
@@ -69,6 +70,12 @@ const widgetFeature = defineFeature("prod-probe", (r) => {
|
|
|
69
70
|
access: { roles: ["anonymous"] },
|
|
70
71
|
handler: async () => ({ pong: true }),
|
|
71
72
|
});
|
|
73
|
+
r.queryHandler({
|
|
74
|
+
name: "kms-probe",
|
|
75
|
+
schema: z.object({}),
|
|
76
|
+
access: { roles: ["anonymous"] },
|
|
77
|
+
handler: async (_event, ctx) => ({ hasKms: ctx.kms !== undefined }),
|
|
78
|
+
});
|
|
72
79
|
// SystemAdmin-gated write — Ziel des extraRoutes.dispatchSystemWrite-
|
|
73
80
|
// Tests: Echo von user.tenantId + roles beweist, dass der Dispatch
|
|
74
81
|
// durch den echten Dispatcher (Zod + Access-Check) läuft und der
|
|
@@ -837,4 +844,43 @@ describe("runProdApp job-lane wiring (runSingleInstance)", () => {
|
|
|
837
844
|
false,
|
|
838
845
|
);
|
|
839
846
|
});
|
|
847
|
+
|
|
848
|
+
test("kms option: adapter reaches handler ctx; without the option ctx.kms is absent", async () => {
|
|
849
|
+
const probe = async (handle: ProdAppHandle): Promise<boolean | undefined> => {
|
|
850
|
+
const res = await handle.entrypoint.app.fetch(
|
|
851
|
+
new Request("http://test/api/query", {
|
|
852
|
+
method: "POST",
|
|
853
|
+
headers: { "content-type": "application/json" },
|
|
854
|
+
body: JSON.stringify({ type: "prod-probe:query:kms-probe", payload: {} }),
|
|
855
|
+
}),
|
|
856
|
+
);
|
|
857
|
+
expect(res.status).toBe(200);
|
|
858
|
+
const body = (await res.json()) as { data?: { hasKms?: boolean } };
|
|
859
|
+
return body.data?.hasKms;
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
const withKms = await boot(undefined, {
|
|
863
|
+
kms: new InMemoryKmsAdapter(),
|
|
864
|
+
anonymousAccess: { defaultTenantId: TENANT_ID },
|
|
865
|
+
});
|
|
866
|
+
expect(await probe(withKms)).toBe(true);
|
|
867
|
+
|
|
868
|
+
const withoutKms = await boot(undefined, {
|
|
869
|
+
anonymousAccess: { defaultTenantId: TENANT_ID },
|
|
870
|
+
});
|
|
871
|
+
expect(await probe(withoutKms)).toBe(false);
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
test("unhealthy kms aborts boot before any connection is opened", async () => {
|
|
875
|
+
const unhealthyKms: KmsAdapter = {
|
|
876
|
+
capabilities: { mode: "local-key" },
|
|
877
|
+
createKey: async () => {},
|
|
878
|
+
getKey: async () => {
|
|
879
|
+
throw new Error("unreachable");
|
|
880
|
+
},
|
|
881
|
+
eraseKey: async () => {},
|
|
882
|
+
health: async () => ({ ok: false, latencyMs: 3 }),
|
|
883
|
+
};
|
|
884
|
+
await expect(boot(undefined, { kms: unhealthyKms })).rejects.toThrow(/KMS health check failed/);
|
|
885
|
+
});
|
|
840
886
|
});
|
package/src/run-prod-app.ts
CHANGED
|
@@ -77,6 +77,7 @@ import {
|
|
|
77
77
|
createSseBroker,
|
|
78
78
|
type SseBroker,
|
|
79
79
|
} from "@cosmicdrift/kumiko-framework/api";
|
|
80
|
+
import type { KmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
|
|
80
81
|
import {
|
|
81
82
|
configureEntityFieldEncryption,
|
|
82
83
|
createDbConnection,
|
|
@@ -477,6 +478,12 @@ export type RunProdAppOptions = {
|
|
|
477
478
|
* Override für KMS-Backends (AWS/GCP/Azure) statt env-KEK. Nur relevant
|
|
478
479
|
* wenn das `secrets`-Feature gemountet ist. */
|
|
479
480
|
readonly masterKey?: MasterKeyProvider;
|
|
481
|
+
/** Subject-Key-Adapter für Crypto-Shredding (DSGVO Art. 17). Wenn gesetzt,
|
|
482
|
+
* steht er Feature-Code als `ctx.kms` zur Verfügung und der Boot prüft
|
|
483
|
+
* seine health() — die App startet nicht gegen ein unerreichbares KMS
|
|
484
|
+
* (Silent-Start würde jeden PII-Read mit 503 beantworten). Kein Default:
|
|
485
|
+
* ohne Adapter bleibt Crypto-Shredding aus. */
|
|
486
|
+
readonly kms?: KmsAdapter;
|
|
480
487
|
/** Deploy-Topologie. Default `true` (Single-Container): dieser Prozess
|
|
481
488
|
* fährt HTTP + BEIDE Job-Lanes (api + worker) + den Event-Dispatcher
|
|
482
489
|
* (MSP-Anwendung) inline — via `createAllInOneEntrypoint`. Damit laufen
|
|
@@ -635,6 +642,7 @@ export function buildBootExtraContext(opts: {
|
|
|
635
642
|
readonly crypto?: BootCrypto;
|
|
636
643
|
readonly masterKey?: MasterKeyProvider;
|
|
637
644
|
readonly sseBroker?: SseBroker;
|
|
645
|
+
readonly kms?: KmsAdapter;
|
|
638
646
|
}): Record<string, unknown> {
|
|
639
647
|
const crypto = opts.crypto ?? resolveBootCrypto(opts.envSource, opts.masterKey);
|
|
640
648
|
const hasSecretsFeature = opts.features.some((f) => f.name === SECRETS_FEATURE_NAME);
|
|
@@ -642,6 +650,7 @@ export function buildBootExtraContext(opts: {
|
|
|
642
650
|
const hasDeliveryFeature = opts.features.some((f) => f.name === DELIVERY_FEATURE);
|
|
643
651
|
return {
|
|
644
652
|
textContent: createTextContentApi(opts.db),
|
|
653
|
+
...(opts.kms && { kms: opts.kms }),
|
|
645
654
|
...(hasDeliveryFeature && {
|
|
646
655
|
_notifyFactory: buildDeliveryNotifyFactory({
|
|
647
656
|
db: opts.db,
|
|
@@ -848,6 +857,18 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
|
|
|
848
857
|
// idempotency, event-dedup, entity-cache, rate-limit; failing to
|
|
849
858
|
// construct here surfaces the misconfig immediately. `new Redis(...)`
|
|
850
859
|
// connects eagerly, so it must stay AFTER the boot-mode exit above.
|
|
860
|
+
// KMS health gate: an app configured for crypto-shredding must not boot
|
|
861
|
+
// against an unreachable key store — a silent start would answer every
|
|
862
|
+
// PII read with 503. Runs BEFORE connections so an abort leaks nothing.
|
|
863
|
+
if (options.kms) {
|
|
864
|
+
const kmsHealth = await options.kms.health();
|
|
865
|
+
if (!kmsHealth.ok) {
|
|
866
|
+
throw new Error(
|
|
867
|
+
`[runProdApp] BOOT ABORTED — KMS health check failed (latency ${kmsHealth.latencyMs}ms)`,
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
851
872
|
const { db, close: closeDb } = createDbConnection(databaseUrl);
|
|
852
873
|
const redis = new Redis(redisUrl, { maxRetriesPerRequest: null });
|
|
853
874
|
|
|
@@ -930,6 +951,7 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
|
|
|
930
951
|
hasAuth: !!effectiveAuth,
|
|
931
952
|
sseBroker,
|
|
932
953
|
crypto: bootCrypto,
|
|
954
|
+
...(options.kms && { kms: options.kms }),
|
|
933
955
|
});
|
|
934
956
|
const extraContext = addConfigAccessorFactory(
|
|
935
957
|
{ ...autoExtraContext, ...resolvedExtraContext },
|