@cosmicdrift/kumiko-server-runtime 0.337.1 → 0.338.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.
@@ -117,8 +117,17 @@ trustedProxyHops = 0) {
117
117
  mtimeMs: file.mtimeMs,
118
118
  };
119
119
  }
120
- function serveHtmlFile(req, pathname, html, extraHeaders) {
120
+ function serveHtmlFile(req, pathname, html, extraHeaders, status) {
121
+ // A not-found page carries no validator: a matching If-None-Match must
122
+ // never turn a 404 into a bodyless 304.
123
+ if (status === 404) {
124
+ return new Response(req.method === "HEAD" ? null : html.bytes, {
125
+ status,
126
+ headers: { "content-type": html.mime, "cache-control": "no-store", ...extraHeaders },
127
+ });
128
+ }
121
129
  return cachedResponse(req, {
130
+ status,
122
131
  body: html.bytes,
123
132
  etag: html.etag,
124
133
  cache: staticCachePolicy(pathname),
@@ -177,30 +186,33 @@ trustedProxyHops = 0) {
177
186
  buildRequestContextDataFromRequest(req, { resolver: clientIpResolver, socketAddress }), () => hostDispatchDispatcher.query(type, payload, createAnonymousUser(tenantId)));
178
187
  };
179
188
  const result = await hostDispatch({ host, path: url.pathname, search: url.search }, { systemQuery });
180
- if (result.kind === "not-found") {
181
- return new Response("Not Found", { status: 404 });
182
- }
183
189
  if (result.kind === "redirect") {
184
190
  return new Response(null, {
185
191
  status: result.status ?? 302,
186
192
  headers: { Location: result.to },
187
193
  });
188
194
  }
189
- // result.kind === "html"
190
- const filePath = `${staticDir}/${result.file}`;
191
- const html = await readHtmlFile(filePath);
195
+ if (result.kind === "not-found") {
196
+ if (result.file === undefined)
197
+ return new Response("Not Found", { status: 404 });
198
+ return serveHostDispatchFile(req, result.file, result.csp, socketAddress, 404);
199
+ }
200
+ return serveHostDispatchFile(req, result.file, result.csp, socketAddress, 200);
201
+ }
202
+ async function serveHostDispatchFile(req, file, csp, socketAddress, status) {
203
+ const html = await readHtmlFile(`${staticDir}/${file}`);
192
204
  if (!html) {
193
205
  // Author-Fehler: hostDispatch verweist auf nicht-existente Datei.
194
206
  // Liefer 500 statt silent-404 damit der Bug schnell auffällt.
195
- return new Response(`hostDispatch: file not found: ${result.file}`, { status: 500 });
207
+ return new Response(`hostDispatch: file not found: ${file}`, { status: 500 });
196
208
  }
197
209
  // Per-host body (hostDispatch picks the file by host) → Vary: Host,
198
210
  // otherwise a shared cache could serve Host A's HTML to Host B.
199
211
  const extraHeaders = { vary: "Host" };
200
- if (result.csp)
201
- extraHeaders["content-security-policy"] = result.csp;
212
+ if (csp)
213
+ extraHeaders["content-security-policy"] = csp;
202
214
  const withHead = await applyPageHead(req, html, socketAddress);
203
- return serveHtmlFile(req, "/index.html", withHead, extraHeaders);
215
+ return serveHtmlFile(req, "/index.html", withHead, extraHeaders, status);
204
216
  }
205
217
  return async (req, socketAddress) => {
206
218
  const url = new URL(req.url);
@@ -179,7 +179,8 @@ export type ExtraContextOption = Record<string, unknown> | ((deps: RunProdAppDep
179
179
  * itself from the authenticated GET /api/schema, after the
180
180
  * clientFeature gates (auth) let it through.
181
181
  * - "redirect": 301/302 to the given location.
182
- * - "not-found": reject outright (e.g. unknown subdomain).
182
+ * - "not-found": reject (e.g. unknown subdomain) with a plain 404, or —
183
+ * with `file` (+ optional `csp`) — serve that page with status 404.
183
184
  *
184
185
  * Only consulted when the path would otherwise fall through to the HTML
185
186
  * fallback — i.e. for "/", "/index.html", or SPA routes that neither Hono
@@ -198,6 +199,8 @@ export type HostDispatchResult = {
198
199
  readonly status?: 301 | 302;
199
200
  } | {
200
201
  readonly kind: "not-found";
202
+ readonly file?: string;
203
+ readonly csp?: string;
201
204
  };
202
205
  export type HostDispatchFn = (req: {
203
206
  readonly host: string;
@@ -242,6 +242,13 @@ export async function runProdApp(options) {
242
242
  const trustedProxyHops = options.trustedProxyHops ??
243
243
  options.auth?.trustedProxyHops ??
244
244
  parseTrustedProxyHopsEnv(readEnv(TRUSTED_PROXY_HOPS_ENV, envSource), "runProdApp");
245
+ if (trustedProxyHops === undefined) {
246
+ // With 0 hops every request appears to come from the proxy pod, so all IP-keyed
247
+ // rate limiters collapse into one shared bucket behind a reverse proxy.
248
+ // biome-ignore lint/suspicious/noConsole: boot-time warn, no logger configured this early
249
+ console.warn(`[runProdApp] trustedProxyHops / ${TRUSTED_PROXY_HOPS_ENV} is not set; client IP falls back to the socket address. ` +
250
+ "Behind a reverse proxy all clients share one rate-limit bucket.");
251
+ }
245
252
  const port = options.port ?? Number.parseInt(envSource["PORT"] ?? "3000", 10);
246
253
  // biome-ignore lint/suspicious/noConsole: boot-time progress hint, no logger configured this early
247
254
  console.log(`[runProdApp] booting Kumiko stack on port ${port}…`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-server-runtime",
3
- "version": "0.337.1",
3
+ "version": "0.338.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,10 +80,10 @@
80
80
  }
81
81
  },
82
82
  "dependencies": {
83
- "@cosmicdrift/kumiko-bundled-features": "0.337.1",
84
- "@cosmicdrift/kumiko-framework": "0.337.1",
85
- "@cosmicdrift/kumiko-headless": "0.337.1",
86
- "@cosmicdrift/kumiko-renderer-web": "0.337.1",
83
+ "@cosmicdrift/kumiko-bundled-features": "0.338.0",
84
+ "@cosmicdrift/kumiko-framework": "0.338.0",
85
+ "@cosmicdrift/kumiko-headless": "0.338.0",
86
+ "@cosmicdrift/kumiko-renderer-web": "0.338.0",
87
87
  "bun-types": "^1.3.13",
88
88
  "ioredis": "^5.10.1",
89
89
  "temporal-polyfill": "^0.3.2"
package/src/changes.json CHANGED
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "version": "0.338.0",
4
+ "type": "improvement",
5
+ "title": "hostDispatch not-found can serve a custom 404 page",
6
+ "detail": "`HostDispatchResult` `{ kind: \"not-found\" }` accepts `file` (relative to `staticDir`) and `csp`. The page goes through the same delivery as `kind: \"html\"` (page head, `Vary: Host`, CSP) with status 404. A missing file answers 500 with the same message as `html`.",
7
+ "migration": "No code change needed."
8
+ },
9
+ {
10
+ "version": "0.338.0",
11
+ "type": "improvement",
12
+ "title": "boot warning when trustedProxyHops is unset",
13
+ "detail": "runProdApp warns at boot when neither `trustedProxyHops` nor `KUMIKO_TRUSTED_PROXY_HOPS` is set. Behind a reverse proxy all clients then share one IP-keyed rate-limit bucket.",
14
+ "migration": "Set `KUMIKO_TRUSTED_PROXY_HOPS` (for example 1 behind one ingress) or pass `trustedProxyHops` to runProdApp."
15
+ },
2
16
  {
3
17
  "version": "0.335.0",
4
18
  "type": "fix",