@cosmicdrift/kumiko-server-runtime 0.382.0 → 0.383.1

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.
@@ -5,7 +5,7 @@
5
5
  // share every step that decides ciphers, blind indexes and config encryption.
6
6
  import { loadJwtSecretOrKeyring } from "@cosmicdrift/kumiko-framework/api";
7
7
  import { configureBlindIndexKey, configurePiiSubjectKms, resolvePlatformKeks, } from "@cosmicdrift/kumiko-framework/crypto";
8
- import { configureEntityFieldEncryption, createDbConnection, dbConnectionOptionsFromEnv, outsideTransactionPoolOptionsFromEnv, } from "@cosmicdrift/kumiko-framework/db";
8
+ import { configureEntityFieldEncryption, createDbConnection, dbConnectionOptionsFromEnv, outsideTransactionPoolOptionsFromEnv, serverTimeoutOptionsFromEnv, } from "@cosmicdrift/kumiko-framework/db";
9
9
  import { createRegistry, findTierResolverUsage, requireTierResolverPlugin, validateBoot, } from "@cosmicdrift/kumiko-framework/engine";
10
10
  import { createWorkerEntrypoint, } from "@cosmicdrift/kumiko-framework/entrypoint";
11
11
  import { assertKumikoSchemaCurrent, SchemaDriftError, } from "@cosmicdrift/kumiko-framework/migrations";
@@ -108,10 +108,12 @@ export async function bootWorkerProcess(options, envSource, profile) {
108
108
  const { db, close: closeDb } = createDbConnection({
109
109
  url: databaseUrl,
110
110
  ...dbConnectionOptionsFromEnv(envSource),
111
+ ...serverTimeoutOptionsFromEnv(envSource),
111
112
  });
112
113
  const { db: outsideTransactionDb, close: closeOutsideTransactionDb } = createDbConnection({
113
114
  url: databaseUrl,
114
115
  ...outsideTransactionPoolOptionsFromEnv(envSource),
116
+ ...serverTimeoutOptionsFromEnv(envSource),
115
117
  });
116
118
  const redis = new Redis(redisUrl, { maxRetriesPerRequest: null });
117
119
  const cacheSync = createRedisCacheSyncBus({
@@ -0,0 +1,8 @@
1
+ import { type Logger } from "@cosmicdrift/kumiko-framework/logging";
2
+ export type FatalErrorHandlersOptions = {
3
+ readonly logger: Pick<Logger, "error">;
4
+ readonly stop: () => Promise<void>;
5
+ readonly exit?: (code: number) => void;
6
+ readonly hardExitMs?: number;
7
+ };
8
+ export declare function installFatalErrorHandlers(options: FatalErrorHandlersOptions): () => void;
@@ -0,0 +1,48 @@
1
+ import { scrubLogText } from "@cosmicdrift/kumiko-framework/logging";
2
+ const DEFAULT_HARD_EXIT_MS = 10_000;
3
+ function describeFatal(reason) {
4
+ if (reason instanceof Error) {
5
+ return {
6
+ message: scrubLogText(reason.message),
7
+ ...(reason.stack !== undefined && { stack: scrubLogText(reason.stack) }),
8
+ };
9
+ }
10
+ return { message: scrubLogText(String(reason)) };
11
+ }
12
+ // A process in an unknown state must not keep serving; the hard-exit timer
13
+ // covers a stop() that hangs on the very fault that triggered it.
14
+ export function installFatalErrorHandlers(options) {
15
+ const { logger, stop } = options;
16
+ const exit = options.exit ?? ((code) => process.exit(code));
17
+ const hardExitMs = options.hardExitMs ?? DEFAULT_HARD_EXIT_MS;
18
+ let handling = false;
19
+ const handle = (source) => (reason) => {
20
+ // skip: a second fault during shutdown is already covered by the running stop and hard-exit timer
21
+ if (handling)
22
+ return;
23
+ handling = true;
24
+ logger.error("fatal process error, shutting down", {
25
+ fatal: true,
26
+ source,
27
+ ...describeFatal(reason),
28
+ });
29
+ const hardExitTimer = setTimeout(() => exit(1), hardExitMs);
30
+ hardExitTimer.unref();
31
+ void stop()
32
+ .catch((stopError) => {
33
+ logger.error("shutdown after fatal error failed", describeFatal(stopError));
34
+ })
35
+ .finally(() => {
36
+ clearTimeout(hardExitTimer);
37
+ exit(1);
38
+ });
39
+ };
40
+ const onUncaughtException = handle("uncaughtException");
41
+ const onUnhandledRejection = handle("unhandledRejection");
42
+ process.on("uncaughtException", onUncaughtException);
43
+ process.on("unhandledRejection", onUnhandledRejection);
44
+ return () => {
45
+ process.off("uncaughtException", onUncaughtException);
46
+ process.off("unhandledRejection", onUnhandledRejection);
47
+ };
48
+ }
package/dist/index.d.ts CHANGED
@@ -13,6 +13,7 @@ export { assertStackBootRequirements } from "./boot-options/stack-boot-requireme
13
13
  export { type BundledAssetDeclaration, type BundledAssetOptions, readBundledAsset, resolveBundledAsset, } from "./bundled-assets.ts";
14
14
  export { type ComposeFeaturesOptions, composeFeatures } from "./compose-features.ts";
15
15
  export { type DiskFileRouteOptions, diskFileRoute } from "./disk-file-route.ts";
16
+ export { type FatalErrorHandlersOptions, installFatalErrorHandlers, } from "./fatal-error-handlers.ts";
16
17
  export type { RateLimitOption } from "./rate-limit-defaults.ts";
17
18
  export type { RunBootstrapOptions } from "./run-bootstrap.ts";
18
19
  export { runBootstrap } from "./run-bootstrap.ts";
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ export { assertStackBootRequirements } from "./boot-options/stack-boot-requireme
12
12
  export { readBundledAsset, resolveBundledAsset, } from "./bundled-assets.js";
13
13
  export { composeFeatures } from "./compose-features.js";
14
14
  export { diskFileRoute } from "./disk-file-route.js";
15
+ export { installFatalErrorHandlers, } from "./fatal-error-handlers.js";
15
16
  export { runBootstrap } from "./run-bootstrap.js";
16
17
  export { parseRunMode, requireEnv, runProdApp } from "./run-prod-app.js";
17
18
  export { runWorkerApp } from "./run-worker-app.js";
@@ -0,0 +1,3 @@
1
+ import { type Logger } from "@cosmicdrift/kumiko-framework/logging";
2
+ export declare function installProcessLogger(extraContext: unknown, consolePrefix: string): Logger;
3
+ export declare function uninstallProcessLogger(): void;
@@ -0,0 +1,42 @@
1
+ import { createLogger, setProcessLogger } from "@cosmicdrift/kumiko-framework/logging";
2
+ import { loggerFromExtraContext } from "./run-prod-app-boot-context.js";
3
+ const CONSOLE_METHOD_BY_LEVEL = {
4
+ info: "log",
5
+ warn: "warn",
6
+ error: "error",
7
+ debug: "debug",
8
+ };
9
+ function createConsoleLogger(prefix) {
10
+ const write = (level) => (msg, data) => {
11
+ const method = CONSOLE_METHOD_BY_LEVEL[level];
12
+ const line = `[${prefix}] ${msg}`;
13
+ // biome-ignore lint/suspicious/noConsole: outside production, boot output stays human-readable on the console
14
+ console[method](...(data ? [line, data] : [line]));
15
+ };
16
+ return {
17
+ info: write("info"),
18
+ warn: write("warn"),
19
+ error: write("error"),
20
+ debug: write("debug"),
21
+ child: () => createConsoleLogger(prefix),
22
+ };
23
+ }
24
+ // Function-form extraContext needs boot deps, so only a static one can supply the logger this early.
25
+ // Outside production no process logger is installed, so fallback loggers keep their console output.
26
+ export function installProcessLogger(extraContext, consolePrefix) {
27
+ const appLogger = typeof extraContext === "object" && extraContext !== null
28
+ ? loggerFromExtraContext(Object.fromEntries(Object.entries(extraContext)))
29
+ : undefined;
30
+ if (appLogger) {
31
+ setProcessLogger(appLogger);
32
+ return appLogger;
33
+ }
34
+ if (process.env["NODE_ENV"] !== "production")
35
+ return createConsoleLogger(consolePrefix);
36
+ const logger = createLogger({ pretty: false });
37
+ setProcessLogger(logger);
38
+ return logger;
39
+ }
40
+ export function uninstallProcessLogger() {
41
+ setProcessLogger(null);
42
+ }
@@ -47,18 +47,18 @@ import { PAT_FEATURE, patRateLimitFromFeature, } from "@cosmicdrift/kumiko-bundl
47
47
  import { SESSIONS_FEATURE } from "@cosmicdrift/kumiko-bundled-features/sessions";
48
48
  import { TenantQueries } from "@cosmicdrift/kumiko-bundled-features/tenant";
49
49
  import { UserQueries } from "@cosmicdrift/kumiko-bundled-features/user";
50
- import { createDefaultSseBroker, createRedisLoginRateLimiter, loadJwtSecretOrKeyring, parseTrustedProxyHopsEnv, TRUSTED_PROXY_HOPS_ENV, withFileProviderResolver, } from "@cosmicdrift/kumiko-framework/api";
50
+ import { createDefaultSseBroker, createRedisLoginRateLimiter, loadJwtSecretOrKeyring, TRUSTED_PROXY_HOPS_ENV, withFileProviderResolver, } from "@cosmicdrift/kumiko-framework/api";
51
51
  import { configureBlindIndexKey, configurePiiSubjectKms, } from "@cosmicdrift/kumiko-framework/crypto";
52
- import { configureEntityFieldEncryption, createDbConnection, dbConnectionOptionsFromEnv, outsideTransactionPoolOptionsFromEnv, } from "@cosmicdrift/kumiko-framework/db";
52
+ import { configureEntityFieldEncryption, createDbConnection, dbConnectionOptionsFromEnv, outsideTransactionPoolOptionsFromEnv, serverTimeoutOptionsFromEnv, } from "@cosmicdrift/kumiko-framework/db";
53
53
  import { collectWriteHandlerQns, createRegistry, findTierResolverUsage, requireTierResolverPlugin, validateAppCustomScreenWriteQns, validateBoot, } from "@cosmicdrift/kumiko-framework/engine";
54
54
  import { createAllInOneEntrypoint, createApiEntrypoint, } from "@cosmicdrift/kumiko-framework/entrypoint";
55
55
  import { createEsOperationsTable, createSeedMigrationContext, runPendingSeedMigrations, } from "@cosmicdrift/kumiko-framework/es-ops";
56
56
  import { assertSeedModeNotInProduction } from "@cosmicdrift/kumiko-framework/event-store";
57
- import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
57
+ import { createFallbackLogger, setProcessLogger } from "@cosmicdrift/kumiko-framework/logging";
58
58
  import { assertKumikoSchemaCurrent, SchemaDriftError, } from "@cosmicdrift/kumiko-framework/migrations";
59
59
  import { createMetricsForFactory, } from "@cosmicdrift/kumiko-framework/observability";
60
60
  import { createDispatcher, createEntityCache, createEventDedup, createIdempotencyGuard, createUnlimitedRateLimitResolver, flushEscapeHatchAudits, } from "@cosmicdrift/kumiko-framework/pipeline";
61
- import { createRedisCacheSyncBus, redisClientOptionsFromEnv, } from "@cosmicdrift/kumiko-framework/redis";
61
+ import { createRedisCacheSyncBus, logRedisConnectionState, redisClientOptionsFromEnv, } from "@cosmicdrift/kumiko-framework/redis";
62
62
  import { warnIfNonUtcServerTimeZone } from "@cosmicdrift/kumiko-framework/time";
63
63
  import { Redis } from "ioredis";
64
64
  import { applyBootSeeds } from "./boot/apply-boot-seeds.js";
@@ -74,7 +74,9 @@ import { assertStackBootRequirements } from "./boot-options/stack-boot-requireme
74
74
  import { buildBunServeOptions, resolveDerivedMaxRequestBodySize } from "./bun-serve-options.js";
75
75
  import { buildComposeAuthOptions, composeFeatures } from "./compose-features.js";
76
76
  import { makeDispatchSystemQuery, makeDispatchSystemWrite, } from "./extra-routes-deps.js";
77
+ import { installFatalErrorHandlers } from "./fatal-error-handlers.js";
77
78
  import { assertPiiBootInvariants } from "./pii-boot-gate.js";
79
+ import { installProcessLogger, uninstallProcessLogger } from "./process-logger.js";
78
80
  import { resolveRateLimit } from "./rate-limit-defaults.js";
79
81
  import { resolveAuthPublicOrigin } from "./resolve-auth-public-origin.js";
80
82
  import { resolveUnsubscribeWiring } from "./resolve-unsubscribe-wiring.js";
@@ -84,6 +86,7 @@ import { withSecurityHeaders } from "./security-headers.js";
84
86
  import { makeSeedCreateFileRef, withSeedErrorContext, } from "./seed-dispatcher.js";
85
87
  import { assertSessionBootInvariants } from "./session-boot-gate.js";
86
88
  import { shouldWireProdSessions } from "./session-wiring.js";
89
+ import { resolveTrustedProxyHops } from "./trusted-proxy-hops.js";
87
90
  import { stripNoRouteMatchHeader } from "./try-hono-first.js";
88
91
  export { parseRunMode } from "./boot/env-gate.js";
89
92
  export { buildBunServeOptions } from "./bun-serve-options.js";
@@ -130,14 +133,28 @@ function makeDryRunHandle() {
130
133
  stop: noop,
131
134
  };
132
135
  }
136
+ const SERVER_GRACEFUL_STOP_MS = 10_000;
137
+ // stop() without force waits for open connections (SSE never ends on its own); force-close after the grace period.
138
+ async function stopServerGracefully(server) {
139
+ let graceTimer;
140
+ const graceElapsed = new Promise((resolve) => {
141
+ graceTimer = setTimeout(resolve, SERVER_GRACEFUL_STOP_MS);
142
+ });
143
+ try {
144
+ await Promise.race([server.stop(), graceElapsed]);
145
+ }
146
+ finally {
147
+ clearTimeout(graceTimer);
148
+ }
149
+ await server.stop(true);
150
+ }
133
151
  let warnedLegacyJwtSecret = false;
134
152
  function warnLegacyJwtSecretOnce() {
135
153
  // skip: already warned once this process — avoid log spam on every boot path.
136
154
  if (warnedLegacyJwtSecret)
137
155
  return;
138
156
  warnedLegacyJwtSecret = true;
139
- // biome-ignore lint/suspicious/noConsole: boot-time ops hint, no logger configured this early
140
- console.warn("[runProdApp] JWT keyring carries a legacy (pre-rotation) verify-only key — " +
157
+ createFallbackLogger("runProdApp").warn("JWT keyring carries a legacy (pre-rotation) verify-only key — " +
141
158
  "it never expires on its own. JWT_SECRET remains a required env (hmacSecret " +
142
159
  "fallback + boot requireEnv). To stop verifying with the pre-rotation secret: " +
143
160
  "set auth.mail.hmacSecret explicitly, then rotate JWT_SECRET to a fresh value " +
@@ -167,6 +184,7 @@ export async function runProdApp(options) {
167
184
  if (envGate.kind === "stopped")
168
185
  return makeDryRunHandle();
169
186
  const { runMode, decryptedEnvSource } = envGate;
187
+ const log = installProcessLogger(options.extraContext, "runProdApp");
170
188
  // Stacks whose boot options are missing and the declarative boot options
171
189
  // (mail, kmsMode, fileStorage, billing, demoMode, seed shortcuts) fail here, before any connection. Their defaults sit under
172
190
  // the env so an explicitly set variable wins.
@@ -204,20 +222,21 @@ export async function runProdApp(options) {
204
222
  // kumiko-framework#1539 — options.auth.trustedProxyHops wins; falls back
205
223
  // to the env var so ops can close the XFF-spoofing hole per-deployment
206
224
  // without a code change (mirrors instanceId's env-first pattern above).
207
- const trustedProxyHops = options.trustedProxyHops ??
208
- options.auth?.trustedProxyHops ??
209
- parseTrustedProxyHopsEnv(readEnv(TRUSTED_PROXY_HOPS_ENV, envSource), "runProdApp");
225
+ const trustedProxyHops = resolveTrustedProxyHops({
226
+ option: options.trustedProxyHops,
227
+ authOption: options.auth?.trustedProxyHops,
228
+ envValue: readEnv(TRUSTED_PROXY_HOPS_ENV, envSource),
229
+ nodeEnv: process.env["NODE_ENV"],
230
+ });
210
231
  if (trustedProxyHops === undefined) {
211
232
  // With 0 hops every request appears to come from the proxy pod, so all IP-keyed
212
233
  // rate limiters collapse into one shared bucket behind a reverse proxy.
213
- // biome-ignore lint/suspicious/noConsole: boot-time warn, no logger configured this early
214
- console.warn(`[runProdApp] trustedProxyHops / ${TRUSTED_PROXY_HOPS_ENV} is not set; client IP falls back to the socket address. ` +
234
+ log.warn(`trustedProxyHops / ${TRUSTED_PROXY_HOPS_ENV} is not set; client IP falls back to the socket address. ` +
215
235
  "Behind a reverse proxy all clients share one rate-limit bucket, so the default L1/L2 limits throttle the whole site at once.");
216
236
  }
217
237
  const resolvedRateLimit = resolveRateLimit(options.rateLimit);
218
238
  const port = options.port ?? Number.parseInt(envSource["PORT"] ?? "3000", 10);
219
- // biome-ignore lint/suspicious/noConsole: boot-time progress hint, no logger configured this early
220
- console.log(`[runProdApp] booting Kumiko stack on port ${port}…`);
239
+ log.info(`booting Kumiko stack on port ${port}`);
221
240
  // auth.mail → expandiert in die expliziten Flow-Felder, bevor sie sowohl
222
241
  // die Feature-Side (buildComposeAuthOptions) als auch das Routes-Fragment
223
242
  // unten speisen. Ab hier IMMER effectiveAuth statt options.auth lesen.
@@ -228,8 +247,7 @@ export async function runProdApp(options) {
228
247
  : undefined;
229
248
  const authPublicOrigin = effectiveAuth
230
249
  ? resolveAuthPublicOrigin(effectiveAuth, envSource, (message) => {
231
- // biome-ignore lint/suspicious/noConsole: boot-time warn, no logger configured this early
232
- console.warn(message);
250
+ log.warn(message);
233
251
  })
234
252
  : undefined;
235
253
  const { unsubscribeLinks, unsubscribeRoutes } = resolveUnsubscribeWiring({
@@ -277,6 +295,7 @@ export async function runProdApp(options) {
277
295
  // biome-ignore lint/suspicious/noConsole: boot-mode output IS the deliverable
278
296
  console.log(`[runProdApp] boot validation OK (${features.length} features, ${registry.features.size} registry entries)`);
279
297
  await bootKms.close();
298
+ uninstallProcessLogger();
280
299
  if (options.envSource === undefined) {
281
300
  process.exit(0);
282
301
  }
@@ -301,12 +320,15 @@ export async function runProdApp(options) {
301
320
  const { db, close: closeDb } = createDbConnection({
302
321
  url: databaseUrl,
303
322
  ...dbConnectionOptionsFromEnv(envSource),
323
+ ...serverTimeoutOptionsFromEnv(envSource),
304
324
  });
305
325
  const { db: outsideTransactionDb, close: closeOutsideTransactionDb } = createDbConnection({
306
326
  url: databaseUrl,
307
327
  ...outsideTransactionPoolOptionsFromEnv(envSource),
328
+ ...serverTimeoutOptionsFromEnv(envSource),
308
329
  });
309
330
  const redis = new Redis(redisUrl, { maxRetriesPerRequest: null });
331
+ logRedisConnectionState(redis, log);
310
332
  await waitForDatabase(db, { processName: "runProdApp" });
311
333
  // Sprint-8a Tier-Composition auto-wire: scan features for a
312
334
  // tenantTierResolver-extension. If found, build the resolver here (db +
@@ -346,15 +368,13 @@ export async function runProdApp(options) {
346
368
  // `migrations: false` für custom Schema-Setups.
347
369
  if (options.migrations !== false) {
348
370
  const migrationsDir = options.migrations?.dir ?? "./kumiko/migrations";
349
- // biome-ignore lint/suspicious/noConsole: boot-time progress hint
350
- console.log(`[runProdApp] checking schema drift (${migrationsDir})…`);
371
+ log.info(`checking schema drift (${migrationsDir})`);
351
372
  try {
352
373
  await assertKumikoSchemaCurrent(db, migrationsDir);
353
374
  }
354
375
  catch (err) {
355
376
  if (err instanceof SchemaDriftError) {
356
- // biome-ignore lint/suspicious/noConsole: terminal error message
357
- console.error(`\n[runProdApp] BOOT ABORTED — ${err.message}\n`);
377
+ log.error(`BOOT ABORTED — ${err.message}`);
358
378
  }
359
379
  throw err;
360
380
  }
@@ -413,6 +433,8 @@ export async function runProdApp(options) {
413
433
  // silent-broken-auth.
414
434
  configureBlindIndexKey(bootKms.blindIndexKey);
415
435
  const bootLogger = loggerFromExtraContext(resolvedExtraContext);
436
+ if (bootLogger)
437
+ setProcessLogger(bootLogger);
416
438
  const autoExtraContext = buildBootExtraContext({
417
439
  db,
418
440
  features,
@@ -769,6 +791,7 @@ export async function runProdApp(options) {
769
791
  const securedUpgradeFetch = withSecurityHeaders(async (req, env) => stripNoRouteMatchHeader(await entrypoint.app.fetch(req, env)), options.securityHeaders, cspScriptHashes);
770
792
  const webSocketUpgradeFetch = async (req, env) => securedUpgradeFetch(req, env);
771
793
  let uninstallShutdownSignals;
794
+ let uninstallFatalHandlers;
772
795
  const handle = {
773
796
  entrypoint,
774
797
  fetch: fetchHandler,
@@ -798,13 +821,20 @@ export async function runProdApp(options) {
798
821
  drainTimeoutMs: options.signals.drainTimeoutMs,
799
822
  }),
800
823
  });
824
+ uninstallFatalHandlers?.();
825
+ uninstallFatalHandlers = installFatalErrorHandlers({
826
+ logger: log,
827
+ stop: () => handle.stop(),
828
+ });
801
829
  }
802
- // biome-ignore lint/suspicious/noConsole: boot-time progress hint, no logger configured this early
803
- console.log(`[runProdApp] ready on http://0.0.0.0:${listenPort}`);
830
+ log.info(`ready on http://0.0.0.0:${listenPort}`);
804
831
  },
805
832
  stop: async () => {
833
+ uninstallFatalHandlers?.();
834
+ uninstallFatalHandlers = undefined;
806
835
  await entrypoint.stop();
807
- handle.server?.stop();
836
+ if (handle.server)
837
+ await stopServerGracefully(handle.server);
808
838
  await flushEscapeHatchAudits();
809
839
  await closeDb();
810
840
  await closeOutsideTransactionDb();
@@ -812,6 +842,7 @@ export async function runProdApp(options) {
812
842
  await bootKms.close();
813
843
  uninstallShutdownSignals?.();
814
844
  uninstallShutdownSignals = undefined;
845
+ uninstallProcessLogger();
815
846
  },
816
847
  };
817
848
  // 12. Auto-listen unless explicitly suppressed (tests pass autoListen:
@@ -15,7 +15,9 @@
15
15
  // Container/Coolify sets the same env vars as runProdApp:
16
16
  // DATABASE_URL, REDIS_URL, JWT_SECRET — PORT is not needed.
17
17
  import { assertSeedModeNotInProduction } from "@cosmicdrift/kumiko-framework/event-store";
18
+ import { scrubLogText } from "@cosmicdrift/kumiko-framework/logging";
18
19
  import { createMetricsForFactory, } from "@cosmicdrift/kumiko-framework/observability";
20
+ import { logRedisConnectionState } from "@cosmicdrift/kumiko-framework/redis";
19
21
  import { runEnvGate } from "./boot/env-gate.js";
20
22
  import { startPiiEventBackfillOnBoot } from "./boot/pii-event-backfill-on-boot.js";
21
23
  import { recordAppStartedOnBoot } from "./boot/record-app-started-on-boot.js";
@@ -24,6 +26,8 @@ import { startWorkerMetricsServer, } from "./boot/worker-metrics-server.js";
24
26
  import { resolveBootOptionWiring } from "./boot-options/resolve-boot-options.js";
25
27
  import { assertStackBootRequirements } from "./boot-options/stack-boot-requirements.js";
26
28
  import { makeDispatchSystemQuery, makeDispatchSystemWrite, } from "./extra-routes-deps.js";
29
+ import { installFatalErrorHandlers } from "./fatal-error-handlers.js";
30
+ import { installProcessLogger, uninstallProcessLogger } from "./process-logger.js";
27
31
  function makeBootModeHandle() {
28
32
  return {
29
33
  // @cast-boundary boot-mode: no entrypoint exists because no boot ran —
@@ -49,8 +53,8 @@ export async function runWorkerApp(options) {
49
53
  const resolvedEnvSource = await resolveGatedEnvSource(options);
50
54
  if (resolvedEnvSource === undefined)
51
55
  return makeBootModeHandle();
52
- // biome-ignore lint/suspicious/noConsole: boot-time progress hint, no logger configured this early
53
- console.log("[runWorkerApp] booting Kumiko worker…");
56
+ const log = installProcessLogger(options.extraContext, "runWorkerApp");
57
+ log.info("booting Kumiko worker");
54
58
  assertWorkerStackRequirements(options);
55
59
  const wiring = await resolveBootOptionWiring(options, resolvedEnvSource);
56
60
  const envSource = Object.keys(wiring.envDefaults).length > 0
@@ -77,9 +81,11 @@ export async function runWorkerApp(options) {
77
81
  }
78
82
  if (boot.kind === "dry-run") {
79
83
  await wiring.kms.close();
84
+ uninstallProcessLogger();
80
85
  return makeBootModeHandle();
81
86
  }
82
87
  const { db, redis, registry, entrypoint } = boot;
88
+ logRedisConnectionState(redis, log);
83
89
  // Metrics first: a port clash must fail the boot before start() begins
84
90
  // consuming jobs/events. boot.close() runs the "metrics-server" shutdown
85
91
  // hook, so a failing start() does not leak the listener.
@@ -97,11 +103,15 @@ export async function runWorkerApp(options) {
97
103
  await wiring.kms.close().catch(() => { });
98
104
  throw error;
99
105
  }
106
+ let uninstallFatalHandlers;
100
107
  const handle = {
101
108
  entrypoint,
102
109
  stop: async () => {
110
+ uninstallFatalHandlers?.();
111
+ uninstallFatalHandlers = undefined;
103
112
  await boot.close();
104
113
  await wiring.kms.close();
114
+ uninstallProcessLogger();
105
115
  },
106
116
  ...(metricsServer && { metricsServer }),
107
117
  };
@@ -130,16 +140,15 @@ export async function runWorkerApp(options) {
130
140
  if (shuttingDown)
131
141
  return;
132
142
  shuttingDown = true;
133
- // biome-ignore lint/suspicious/noConsole: boot-time progress hint, no logger configured this early
134
- console.log(`[runWorkerApp] ${signal} received — draining…`);
143
+ log.info(`${signal} received, draining`);
135
144
  try {
136
145
  await handle.stop();
137
- // biome-ignore lint/suspicious/noConsole: boot-time progress hint, no logger configured this early
138
- console.log("[runWorkerApp] graceful shutdown complete.");
146
+ log.info("graceful shutdown complete");
139
147
  }
140
148
  catch (e) {
141
- // biome-ignore lint/suspicious/noConsole: shutdown-time error, only path is stderr
142
- console.error("[runWorkerApp] error during shutdown:", e);
149
+ log.error("error during shutdown", {
150
+ error: scrubLogText(e instanceof Error ? e.message : String(e)),
151
+ });
143
152
  }
144
153
  finally {
145
154
  process.exit(0);
@@ -147,7 +156,7 @@ export async function runWorkerApp(options) {
147
156
  };
148
157
  process.on("SIGTERM", () => void shutdown("SIGTERM"));
149
158
  process.on("SIGINT", () => void shutdown("SIGINT"));
150
- // biome-ignore lint/suspicious/noConsole: boot-time progress hint, no logger configured this early
151
- console.log("[runWorkerApp] ready — worker running.");
159
+ uninstallFatalHandlers = installFatalErrorHandlers({ logger: log, stop: () => handle.stop() });
160
+ log.info("ready, worker running");
152
161
  return handle;
153
162
  }
@@ -0,0 +1,6 @@
1
+ export declare function resolveTrustedProxyHops(input: {
2
+ readonly option: number | undefined;
3
+ readonly authOption: number | undefined;
4
+ readonly envValue: string | undefined;
5
+ readonly nodeEnv: string | undefined;
6
+ }): number | undefined;
@@ -0,0 +1,9 @@
1
+ import { parseTrustedProxyHopsEnv, TRUSTED_PROXY_HOPS_ENV, } from "@cosmicdrift/kumiko-framework/api";
2
+ export function resolveTrustedProxyHops(input) {
3
+ const hops = input.option ?? input.authOption ?? parseTrustedProxyHopsEnv(input.envValue, "runProdApp");
4
+ if (hops === undefined && input.nodeEnv === "production") {
5
+ throw new Error(`[runProdApp] trustedProxyHops is required in production: pass the trustedProxyHops option or set ${TRUSTED_PROXY_HOPS_ENV} ` +
6
+ "(0 when the app is exposed directly). Without it every client behind a reverse proxy shares one rate-limit bucket.");
7
+ }
8
+ return hops;
9
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-server-runtime",
3
- "version": "0.382.0",
3
+ "version": "0.383.1",
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 tooling.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -88,18 +88,18 @@
88
88
  }
89
89
  },
90
90
  "dependencies": {
91
- "@cosmicdrift/kumiko-bundled-features": "0.382.0",
92
- "@cosmicdrift/kumiko-framework": "0.382.0",
93
- "@cosmicdrift/kumiko-renderer": "0.382.0",
94
- "@cosmicdrift/kumiko-renderer-mail-html": "0.382.0",
95
- "@cosmicdrift/kumiko-types": "0.382.0",
91
+ "@cosmicdrift/kumiko-bundled-features": "0.383.1",
92
+ "@cosmicdrift/kumiko-framework": "0.383.1",
93
+ "@cosmicdrift/kumiko-renderer": "0.383.1",
94
+ "@cosmicdrift/kumiko-renderer-mail-html": "0.383.1",
95
+ "@cosmicdrift/kumiko-types": "0.383.1",
96
96
  "bun-types": "^1.3.13",
97
97
  "ioredis": "^5.10.1",
98
98
  "temporal-polyfill": "^0.3.2",
99
99
  "zod": "^4.4.3"
100
100
  },
101
101
  "devDependencies": {
102
- "@cosmicdrift/kumiko-testing": "0.382.0"
102
+ "@cosmicdrift/kumiko-testing": "0.383.1"
103
103
  },
104
104
  "publishConfig": {
105
105
  "registry": "https://registry.npmjs.org",
package/src/changes.json CHANGED
@@ -1,4 +1,23 @@
1
1
  [
2
+ {
3
+ "version": "0.383.0",
4
+ "type": "improvement",
5
+ "title": "Production logs are plain JSON without the [runProdApp] prefix",
6
+ "detail": "Behavior change for log parsers and alert rules: runProdApp and runWorkerApp now write one JSON object per line from the process logger, without the former \"[runProdApp]\" text prefix. Match on the component field instead of the prefix."
7
+ },
8
+ {
9
+ "version": "0.383.0",
10
+ "type": "breaking",
11
+ "title": "runProdApp requires trustedProxyHops in production",
12
+ "detail": "In production runProdApp refuses to boot when neither the trustedProxyHops option, the auth option nor KUMIKO_TRUSTED_PROXY_HOPS is set. An explicit 0 is allowed. create-kumiko-app templates write KUMIKO_TRUSTED_PROXY_HOPS=0 into .env.example.",
13
+ "migration": "set KUMIKO_TRUSTED_PROXY_HOPS=<hops> (0 when the app is exposed directly) or pass trustedProxyHops"
14
+ },
15
+ {
16
+ "version": "0.383.0",
17
+ "type": "improvement",
18
+ "title": "Fatal-error handlers and graceful server stop",
19
+ "detail": "New installFatalErrorHandlers logs uncaughtException and unhandledRejection, stops the app and exits 1 (hard exit after 10 s). runProdApp and runWorkerApp install it. The HTTP server stop is awaited against a 10 s grace period and then forced before the database closes."
20
+ },
2
21
  {
3
22
  "version": "0.379.0",
4
23
  "type": "improvement",