@cosmicdrift/kumiko-server-runtime 0.320.0 → 0.322.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.320.0",
3
+ "version": "0.322.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>",
@@ -76,9 +76,9 @@
76
76
  }
77
77
  },
78
78
  "dependencies": {
79
- "@cosmicdrift/kumiko-bundled-features": "0.320.0",
80
- "@cosmicdrift/kumiko-framework": "0.320.0",
81
- "@cosmicdrift/kumiko-headless": "0.320.0",
79
+ "@cosmicdrift/kumiko-bundled-features": "0.322.0",
80
+ "@cosmicdrift/kumiko-framework": "0.322.0",
81
+ "@cosmicdrift/kumiko-headless": "0.322.0",
82
82
  "temporal-polyfill": "^0.3.2"
83
83
  },
84
84
  "publishConfig": {
@@ -359,6 +359,27 @@ describe("runProdApp", () => {
359
359
  expect(body).toContain('<probe ok="true" />');
360
360
  });
361
361
 
362
+ test("handle.fetch threads the socket address into deps.clientIp, ignoring a spoofed X-Forwarded-For at the default trustedProxyHops (0)", async () => {
363
+ const handle = await boot(undefined, {
364
+ extraRoutes: [
365
+ {
366
+ method: "GET",
367
+ path: "/client-ip-probe",
368
+ entry: "anonymous",
369
+ handler: (c, deps) => c.body(deps.clientIp, 200),
370
+ },
371
+ ],
372
+ });
373
+ const req = () =>
374
+ new Request("http://test/client-ip-probe", { headers: { "x-forwarded-for": "1.2.3.4" } });
375
+
376
+ const withSocket = await handle.fetch(req(), "198.51.100.7");
377
+ expect(await withSocket.text()).toBe("198.51.100.7");
378
+
379
+ const withoutSocket = await handle.fetch(req());
380
+ expect(await withoutSocket.text()).toBe("unknown");
381
+ });
382
+
362
383
  test("extraRoutes: entry:signature dispatchSystemWrite schreibt als SystemAdmin des Ziel-Tenants, registry verfügbar", async () => {
363
384
  // Wiring for provider webhook routes (billing-foundation
364
385
  // createSubscriptionWebhookRoute): the route authenticates via
package/src/changes.json CHANGED
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "version": "0.321.0",
4
+ "type": "improvement",
5
+ "title": "runProdApp's KUMIKO_TRUSTED_PROXY_HOPS parsing now shares framework's parseTrustedProxyHopsEnv",
6
+ "detail": "The inline digits-only + non-negative-integer validation moved out of\n`run-prod-app.ts` into `parseTrustedProxyHopsEnv`\n(`@cosmicdrift/kumiko-framework/api`); same validation and error message\nas before, no behavioral change.",
7
+ "migration": "No action needed."
8
+ },
9
+ {
10
+ "version": "0.321.0",
11
+ "type": "improvement",
12
+ "title": "ProdAppHandle.fetch's type now declares its optional socket-address parameter",
13
+ "detail": "`fetch: (req: Request, socketAddress?: string) => Promise<Response> |\nResponse`. The doc comment now explains that an app with `autoListen:\nfalse` running its own `Bun.serve` must pass\n`server.requestIP(req)?.address`, extracted at the outermost Bun fetch\ncallback — `requestIP()` only resolves for Bun's original Request\ninstance, not a cloned/rebuilt one. `RunProdAppAuthOptions\n.trustedProxyHops`'s doc comment also dropped the stale \"pre-#1539\nspoofable default\" wording; 0 hops is the safe socket-only default.",
14
+ "migration": "No action needed for the default `autoListen: true` boot. Apps with\n`autoListen: false` can now type-check passing the socket address to\n`handle.fetch`; passing nothing keeps the previous (unresolved) behavior."
15
+ },
2
16
  {
3
17
  "version": "0.320.0",
4
18
  "type": "breaking",
@@ -76,8 +76,10 @@ import {
76
76
  type LoginRateLimiter,
77
77
  loadJwtSecretOrKeyring,
78
78
  type PostAuthLandingResolver,
79
+ parseTrustedProxyHopsEnv,
79
80
  type SseBroker,
80
81
  type TokenVerifier,
82
+ TRUSTED_PROXY_HOPS_ENV,
81
83
  } from "@cosmicdrift/kumiko-framework/api";
82
84
  import {
83
85
  configureBlindIndexKey,
@@ -380,8 +382,10 @@ export type RunProdAppAuthOptions = {
380
382
  * process for client-IP derivation (see AuthRoutesConfig.trustedProxyHops,
381
383
  * kumiko-framework#1539) — closes the X-Forwarded-For spoofing hole on
382
384
  * the auth rate-limiters. Falls back to the `KUMIKO_TRUSTED_PROXY_HOPS`
383
- * env var when unset; both unset means the pre-#1539 spoofable default
384
- * (0 hops). Set this to your real ingress hop count (typically 1). */
385
+ * env var when unset; both unset means 0 hops, the safe socket-only
386
+ * default (only the socket address counts, XFF ignored). Deprecated in
387
+ * favor of the top-level `trustedProxyHops` option, which wins over
388
+ * this and the env var. */
385
389
  readonly trustedProxyHops?: number;
386
390
  /** Server-computed post-auth redirect (see AuthRoutesConfig.postAuthLanding).
387
391
  * Result lands in the login/signup/invite response as `landingPath`; an
@@ -700,8 +704,12 @@ export type ProdAppHandle = {
700
704
  * `process.exit(0)` before returning a handle. */
701
705
  readonly entrypoint: ApiEntrypoint | AllInOneEntrypoint;
702
706
  /** The fetch-handler — wired into Bun.serve in production, called
703
- * directly in tests. Composes Hono + static-fallback. */
704
- readonly fetch: (req: Request) => Promise<Response> | Response;
707
+ * directly in tests. Composes Hono + static-fallback. Apps with
708
+ * `autoListen: false` that call `Bun.serve` themselves must pass the
709
+ * 2nd arg from `server.requestIP(req)?.address`, extracted in their
710
+ * outermost Bun fetch callback — `requestIP` only resolves for Bun's
711
+ * original Request instance, never a cloned/rebuilt one. */
712
+ readonly fetch: (req: Request, socketAddress?: string) => Promise<Response> | Response;
705
713
  /** Active Bun-server (only set when listen() was called — tests skip
706
714
  * listen() because Bun.serve isn't available under vitest/node). */
707
715
  server?: ReturnType<typeof Bun.serve>;
@@ -818,31 +826,10 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
818
826
  // kumiko-framework#1539 — options.auth.trustedProxyHops wins; falls back
819
827
  // to the env var so ops can close the XFF-spoofing hole per-deployment
820
828
  // without a code change (mirrors instanceId's env-first pattern above).
821
- // Fail loud on a garbage env value rather than silently coercing to NaN:
822
- // clientIpOf treats NaN like "always short chain" and returns "unknown"
823
- // for every request, which collapses mfa-verify/preauth-confirm's
824
- // pure-IP-keyed rate limiter into one shared bucket for the whole
825
- // deployment — a self-inflicted DoS, worse than staying on the default.
826
- const trustedProxyHopsFromEnv = readEnv("KUMIKO_TRUSTED_PROXY_HOPS", envSource);
827
- const trustedProxyHops = ((): number | undefined => {
828
- if (options.trustedProxyHops !== undefined) return options.trustedProxyHops;
829
- if (options.auth?.trustedProxyHops !== undefined) return options.auth.trustedProxyHops;
830
- if (trustedProxyHopsFromEnv === undefined) return undefined;
831
- // Digits-only — parseInt("0x10")/("1e3")/("2x") would silently coerce
832
- // and land on the spoofable hops=0 path for values like "0x10".
833
- if (!/^\d+$/.test(trustedProxyHopsFromEnv)) {
834
- throw new Error(
835
- `runProdApp: KUMIKO_TRUSTED_PROXY_HOPS must be a non-negative integer, got "${trustedProxyHopsFromEnv}".`,
836
- );
837
- }
838
- const parsed = Number.parseInt(trustedProxyHopsFromEnv, 10);
839
- if (!Number.isInteger(parsed) || parsed < 0) {
840
- throw new Error(
841
- `runProdApp: KUMIKO_TRUSTED_PROXY_HOPS must be a non-negative integer, got "${trustedProxyHopsFromEnv}".`,
842
- );
843
- }
844
- return parsed;
845
- })();
829
+ const trustedProxyHops =
830
+ options.trustedProxyHops ??
831
+ options.auth?.trustedProxyHops ??
832
+ parseTrustedProxyHopsEnv(readEnv(TRUSTED_PROXY_HOPS_ENV, envSource), "runProdApp");
846
833
  const port = options.port ?? Number.parseInt(envSource["PORT"] ?? "3000", 10);
847
834
 
848
835
  // biome-ignore lint/suspicious/noConsole: boot-time progress hint, no logger configured this early