@cosmicdrift/kumiko-server-runtime 0.351.0 → 0.353.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.
@@ -94,7 +94,6 @@ export declare function assertRendererWebShellPresent(css: string, stylesheet: R
94
94
  export declare function discoverHtmlTemplate(cwd: string): string | undefined;
95
95
  export declare function runTailwindOnce(entry: string, cwd: string): Promise<string>;
96
96
  export declare function isEntryOutputFor(outName: string, baseName: string): boolean;
97
- export declare function buildMissingTemplateError(manifest: BuildManifest, entry: ClientEntry): string;
98
97
  export declare function injectAssetTags(html: string, manifest: BuildManifest, entry: ClientEntry, buildInfo?: BuildInfo): string;
99
98
  export declare function computeBuildId(manifest: BuildManifest): string;
100
99
  /** Formatiert ein BuildResult als CLI-freundliche Mehrzeilen-Zusammenfassung
@@ -672,10 +672,9 @@ async function renderHtml(templatePath, manifest, entry, buildInfo) {
672
672
  const template = templatePath ? await readFile(templatePath, "utf8") : DEFAULT_HTML;
673
673
  return injectAssetTags(template, manifest, entry, buildInfo);
674
674
  }
675
- // @internal — exported for unit tests only. See #2305: the message must
676
- // name the source file and, for a declared multi-entry, which package.json
677
- // "kumiko.clientEntries" entry it came from.
678
- export function buildMissingTemplateError(manifest, entry) {
675
+ // The message must name the source file and, for a declared multi-entry,
676
+ // which package.json "kumiko.clientEntries" entry it came from.
677
+ function buildMissingTemplateError(manifest, entry) {
679
678
  const cssLine = manifest["styles.css"]
680
679
  ? ` <link rel="stylesheet" href="/styles.css" />\n`
681
680
  : "";
@@ -17,7 +17,7 @@ export function assertPiiBootInvariants(features, opts) {
17
17
  // encryptEventPayloadPii no-ops without a KMS, so a catalogued event writes
18
18
  // plaintext PII into kumiko_events with nothing to crypto-shred later.
19
19
  const piiEvents = features.flatMap((feature) => Object.values(feature.events ?? {})
20
- .filter((event) => event.piiFields !== undefined && event.piiFields !== "none")
20
+ .filter((event) => event.piiFields !== "none")
21
21
  .map((event) => event.name));
22
22
  // skip: nothing PII-annotated is mounted — plaintext gate is moot.
23
23
  if (piiEntities.length === 0 && piiEvents.length === 0)
@@ -0,0 +1,12 @@
1
+ import { type ClientIpResolver } from "@cosmicdrift/kumiko-framework/api";
2
+ import { type SessionUser } from "@cosmicdrift/kumiko-framework/engine";
3
+ import type { PageHeadSystemQuery } from "@cosmicdrift/kumiko-headless/apex";
4
+ export type QueryDispatcher = {
5
+ readonly query: (type: string, payload: unknown, user: SessionUser) => Promise<unknown>;
6
+ };
7
+ export declare function buildRequestBoundSystemQuery(params: {
8
+ readonly req: Request;
9
+ readonly dispatcher: QueryDispatcher;
10
+ readonly resolver: ClientIpResolver;
11
+ readonly socketAddress?: string;
12
+ }): PageHeadSystemQuery;
@@ -0,0 +1,9 @@
1
+ import { buildRequestContextDataFromRequest, requestContext, } from "@cosmicdrift/kumiko-framework/api";
2
+ import { createAnonymousUser } from "@cosmicdrift/kumiko-framework/engine";
3
+ // Single wiring for the anonymous-role systemQuery that page-head and
4
+ // hostDispatch resolvers receive, shared by runProdApp and the dev server so
5
+ // the anonymous role / request-context / client-IP resolution can't drift.
6
+ export function buildRequestBoundSystemQuery(params) {
7
+ const { req, dispatcher, resolver, socketAddress } = params;
8
+ return (type, payload, tenantId) => requestContext.run(requestContext.get() ?? buildRequestContextDataFromRequest(req, { resolver, socketAddress }), () => dispatcher.query(type, payload, createAnonymousUser(tenantId)));
9
+ }
@@ -1,5 +1,5 @@
1
1
  import { type CachePolicy } from "@cosmicdrift/kumiko-framework/api";
2
- import { type SessionUser } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { type QueryDispatcher } from "./request-bound-system-query.js";
3
3
  import type { HostDispatchFn, PageHeadResolver } from "./run-prod-app.js";
4
4
  export declare function readStaticFile(filePath: string): Promise<{
5
5
  readonly bytes: Uint8Array;
@@ -12,9 +12,6 @@ export declare function serveDiskFile(req: Request, pathname: string, file: {
12
12
  readonly mtimeMs: number;
13
13
  }): Response;
14
14
  export declare function mimeTypeFor(filePath: string): string;
15
- type QueryDispatcher = {
16
- readonly query: (type: string, payload: unknown, user: SessionUser) => Promise<unknown>;
17
- };
18
15
  export type PageHeadOptions = {
19
16
  readonly resolvePageHead: PageHeadResolver;
20
17
  readonly dispatcher: QueryDispatcher;
@@ -29,4 +26,3 @@ hostDispatchDispatcher?: QueryDispatcher,
29
26
  * 1:1 with runProdApp's effective value. */
30
27
  trustedProxyHops?: number): (req: Request, socketAddress?: string) => Promise<Response>;
31
28
  export declare function staticCachePolicy(pathname: string): CachePolicy;
32
- export {};
@@ -1,9 +1,9 @@
1
1
  import { resolve, sep } from "node:path";
2
- import { buildRequestContextDataFromRequest, cachedResponse, computeStrongEtag, computeWeakEtag, createClientIpResolver, requestContext, } from "@cosmicdrift/kumiko-framework/api";
3
- import { createAnonymousUser } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { cachedResponse, computeStrongEtag, computeWeakEtag, createClientIpResolver, } from "@cosmicdrift/kumiko-framework/api";
4
3
  import { resolveAndInjectPageHead } from "@cosmicdrift/kumiko-headless/apex";
5
4
  import { ASSETS_DIR } from "./build-prod-bundle.js";
6
5
  import { BUNDLED_ASSETS_DIST_DIR } from "./bundled-assets.js";
6
+ import { buildRequestBoundSystemQuery, } from "./request-bound-system-query.js";
7
7
  import { stripNoRouteMatchHeader, tryHonoFirst } from "./try-hono-first.js";
8
8
  // Static-asset + SPA-fallback serving for runProdApp's HTTP handler. Split
9
9
  // out of run-prod-app.ts (#1005, Welle 2) — mechanical relocation, these
@@ -162,8 +162,12 @@ trustedProxyHops = 0) {
162
162
  return html;
163
163
  const url = new URL(req.url);
164
164
  const host = req.headers.get("host") ?? url.host;
165
- const systemQuery = (type, payload, tenantId) => requestContext.run(requestContext.get() ??
166
- buildRequestContextDataFromRequest(req, { resolver: clientIpResolver, socketAddress }), () => pageHead.dispatcher.query(type, payload, createAnonymousUser(tenantId)));
165
+ const systemQuery = buildRequestBoundSystemQuery({
166
+ req,
167
+ dispatcher: pageHead.dispatcher,
168
+ resolver: clientIpResolver,
169
+ socketAddress,
170
+ });
167
171
  const text = new TextDecoder().decode(html.bytes);
168
172
  const injected = await resolveAndInjectPageHead(text, pageHead.resolvePageHead, {
169
173
  path: url.pathname,
@@ -192,8 +196,12 @@ trustedProxyHops = 0) {
192
196
  if (!hostDispatchDispatcher) {
193
197
  throw new Error("hostDispatch called deps.systemQuery but buildStaticFallback got no hostDispatchDispatcher");
194
198
  }
195
- return requestContext.run(requestContext.get() ??
196
- buildRequestContextDataFromRequest(req, { resolver: clientIpResolver, socketAddress }), () => hostDispatchDispatcher.query(type, payload, createAnonymousUser(tenantId)));
199
+ return buildRequestBoundSystemQuery({
200
+ req,
201
+ dispatcher: hostDispatchDispatcher,
202
+ resolver: clientIpResolver,
203
+ socketAddress,
204
+ })(type, payload, tenantId);
197
205
  };
198
206
  const result = await hostDispatch({ host, path: url.pathname, search: url.search }, { systemQuery });
199
207
  if (result.kind === "redirect") {
@@ -176,6 +176,28 @@ export async function runProdApp(options) {
176
176
  // needed in 9.1's audit (37 references, 25 distinct vars).
177
177
  const rawEnvSource = options.envSource ?? process.env;
178
178
  const runMode = parseRunMode(rawEnvSource["KUMIKO_DRY_RUN_ENV"]);
179
+ const composedEnvSchema = options.envSchema;
180
+ const validateEnv = (source) => {
181
+ // skip: app declares no env schema, nothing to validate
182
+ if (!composedEnvSchema)
183
+ return;
184
+ try {
185
+ const schema = options.masterKey
186
+ ? withOptionalEnvKeys(composedEnvSchema.schema, SECRETS_MASTER_KEK_ENV_KEYS)
187
+ : composedEnvSchema.schema;
188
+ parseEnv(schema, source, {
189
+ sources: composedEnvSchema.sources,
190
+ ...(options.pulumiPrefix ? { pulumiPrefix: options.pulumiPrefix } : {}),
191
+ });
192
+ }
193
+ catch (err) {
194
+ if (err instanceof KumikoBootError) {
195
+ const reporter = options.bootErrorReporter ?? defaultBootErrorReporter;
196
+ reporter(err);
197
+ }
198
+ throw err;
199
+ }
200
+ };
179
201
  if (options.envSchema) {
180
202
  if (isRenderMode(runMode)) {
181
203
  // biome-ignore lint/suspicious/noConsole: dry-run output IS the deliverable
@@ -194,22 +216,7 @@ export async function runProdApp(options) {
194
216
  // boot-mode AND normal-boot both run env-validation. boot-mode wants
195
217
  // a real env-check (all required vars present + schema-valid) before
196
218
  // it asserts feature-wiring works.
197
- try {
198
- const envSchema = options.masterKey
199
- ? withOptionalEnvKeys(options.envSchema.schema, SECRETS_MASTER_KEK_ENV_KEYS)
200
- : options.envSchema.schema;
201
- parseEnv(envSchema, rawEnvSource, {
202
- sources: options.envSchema.sources,
203
- ...(options.pulumiPrefix ? { pulumiPrefix: options.pulumiPrefix } : {}),
204
- });
205
- }
206
- catch (err) {
207
- if (err instanceof KumikoBootError) {
208
- const reporter = options.bootErrorReporter ?? defaultBootErrorReporter;
209
- reporter(err);
210
- }
211
- throw err;
212
- }
219
+ validateEnv(rawEnvSource);
213
220
  }
214
221
  // Slots the schema marks `kms` are decrypted once here so every consumer
215
222
  // below (boot probe, master-key keyring, config) sees the same resolved env.
@@ -219,6 +226,12 @@ export async function runProdApp(options) {
219
226
  logPrefix: "[runProdApp]",
220
227
  })
221
228
  : rawEnvSource;
229
+ // The first parse relaxes ciphertext-only slots, so their decrypted
230
+ // plaintext never met the slot's own validators (.regex/.min/.refine).
231
+ if (options.envSchema &&
232
+ kmsSlotsOf(options.envSchema.schema).some((slot) => rawEnvSource[slot] === undefined && envSource[slot] !== undefined)) {
233
+ validateEnv(envSource);
234
+ }
222
235
  // 1. Polyfill before anything else — feature code references Temporal.
223
236
  const { ensureTemporalPolyfill } = await import("@cosmicdrift/kumiko-framework/time");
224
237
  await ensureTemporalPolyfill();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-server-runtime",
3
- "version": "0.351.0",
3
+ "version": "0.353.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>",
@@ -74,17 +74,21 @@
74
74
  "types": "./dist/try-hono-first.d.ts",
75
75
  "default": "./dist/try-hono-first.js"
76
76
  },
77
+ "./request-bound-system-query": {
78
+ "types": "./dist/request-bound-system-query.d.ts",
79
+ "default": "./dist/request-bound-system-query.js"
80
+ },
77
81
  "./build-prod-bundle": {
78
82
  "types": "./dist/build-prod-bundle.d.ts",
79
83
  "default": "./dist/build-prod-bundle.js"
80
84
  }
81
85
  },
82
86
  "dependencies": {
83
- "@cosmicdrift/kumiko-bundled-features": "0.351.0",
84
- "@cosmicdrift/kumiko-framework": "0.351.0",
85
- "@cosmicdrift/kumiko-headless": "0.351.0",
86
- "@cosmicdrift/kumiko-renderer-web": "0.351.0",
87
- "@cosmicdrift/kumiko-types": "0.351.0",
87
+ "@cosmicdrift/kumiko-bundled-features": "0.353.0",
88
+ "@cosmicdrift/kumiko-framework": "0.353.0",
89
+ "@cosmicdrift/kumiko-headless": "0.353.0",
90
+ "@cosmicdrift/kumiko-renderer-web": "0.353.0",
91
+ "@cosmicdrift/kumiko-types": "0.353.0",
88
92
  "bun-types": "^1.3.13",
89
93
  "ioredis": "^5.10.1",
90
94
  "temporal-polyfill": "^0.3.2"
@@ -149,6 +153,10 @@
149
153
  "types": "./dist/try-hono-first.d.ts",
150
154
  "default": "./dist/try-hono-first.js"
151
155
  },
156
+ "./request-bound-system-query": {
157
+ "types": "./dist/request-bound-system-query.d.ts",
158
+ "default": "./dist/request-bound-system-query.js"
159
+ },
152
160
  "./build-prod-bundle": {
153
161
  "types": "./dist/build-prod-bundle.d.ts",
154
162
  "default": "./dist/build-prod-bundle.js"
package/src/changes.json CHANGED
@@ -1,4 +1,16 @@
1
1
  [
2
+ {
3
+ "version": "0.353.0",
4
+ "type": "fix",
5
+ "title": "Boot rejects decrypted kms env slots that violate their schema validators",
6
+ "detail": "runProdApp validates Key-Manager-decrypted env slots against their schema\nA `kms` slot delivered only as `_CIPHERTEXT` skipped its own `.regex`/`.min`/`.refine` because the first parse relaxes it. After decryption the env is now parsed again, so a malformed plaintext aborts the boot with a `KumikoBootError` naming the slot."
7
+ },
8
+ {
9
+ "version": "0.353.0",
10
+ "type": "improvement",
11
+ "title": "Prod static files and the dev server share one request-bound systemQuery wiring",
12
+ "detail": "Shared request-bound systemQuery wiring and clearer deploy scaffold warnings\n`@cosmicdrift/kumiko-server-runtime/request-bound-system-query` exports the systemQuery wiring that the prod static-file path and the dev server previously each copied. `scaffoldDeploy` now tells a broken `package.json` (deploy settings fall back to defaults) apart from an unexpected `dependencies`/`workspaces` shape in its warning."
13
+ },
2
14
  {
3
15
  "version": "0.347.0",
4
16
  "type": "fix",