@cosmicdrift/kumiko-server-runtime 0.342.0 → 0.344.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.
@@ -16,7 +16,7 @@ import { Redis } from "ioredis";
16
16
  import { composeFeatures } from "../compose-features.js";
17
17
  import { assertPiiBootInvariants } from "../pii-boot-gate.js";
18
18
  import { requireEnv } from "../run-prod-app.js";
19
- import { addConfigAccessorFactory, buildBootExtraContext } from "../run-prod-app-boot-context.js";
19
+ import { addConfigAccessorFactory, buildBootExtraContext, loggerFromExtraContext, } from "../run-prod-app-boot-context.js";
20
20
  import { resolveBootCrypto } from "./boot-crypto.js";
21
21
  import { jobRunLoggerCallbacks } from "./job-run-logger.js";
22
22
  import { assertWorkerMetricsOptions } from "./worker-metrics-server.js";
@@ -71,7 +71,12 @@ export async function bootWorkerProcess(options, envSource, profile) {
71
71
  includeBundled,
72
72
  ...(profile.authOptions && { authOptions: profile.authOptions }),
73
73
  });
74
- validateBoot(features, { env: envSource, ...options.validateBootOptions });
74
+ const bootCrypto = resolveBootCrypto(envSource, options.masterKey);
75
+ validateBoot(features, {
76
+ env: envSource,
77
+ ...(bootCrypto.entityFieldCipher && { entityFieldCipher: bootCrypto.entityFieldCipher }),
78
+ ...options.validateBootOptions,
79
+ });
75
80
  warnIfNonUtcServerTimeZone();
76
81
  assertWorkerMetricsOptions(options.metrics, options.observability, processName);
77
82
  assertPiiBootInvariants(features, {
@@ -124,10 +129,10 @@ export async function bootWorkerProcess(options, envSource, profile) {
124
129
  const resolvedExtraContext = typeof options.extraContext === "function"
125
130
  ? options.extraContext(deps)
126
131
  : (options.extraContext ?? {});
127
- const bootCrypto = resolveBootCrypto(envSource, options.masterKey);
128
132
  configureEntityFieldEncryption(bootCrypto.entityFieldCipher);
129
133
  configurePiiSubjectKms(options.kms);
130
134
  configureBlindIndexKey(options.blindIndexKey);
135
+ const bootLogger = loggerFromExtraContext(resolvedExtraContext);
131
136
  const autoExtraContext = buildBootExtraContext({
132
137
  db,
133
138
  features,
@@ -137,6 +142,7 @@ export async function bootWorkerProcess(options, envSource, profile) {
137
142
  crypto: bootCrypto,
138
143
  ...(options.kms && { kms: options.kms }),
139
144
  ...(profile.deliverQueuedInline === true && { deliverQueuedInline: true }),
145
+ ...(bootLogger && { log: bootLogger }),
140
146
  });
141
147
  const extraContext = addConfigAccessorFactory({ ...autoExtraContext, ...resolvedExtraContext }, registry);
142
148
  const jobLogger = jobRunLoggerCallbacks(registry, db);
@@ -48,9 +48,8 @@
48
48
  // alles andere (public/) → default (auto-cache)
49
49
  import { createHash } from "node:crypto";
50
50
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
51
- import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
52
- import { tmpdir } from "node:os";
53
- import { isAbsolute, join, relative, resolve, sep } from "node:path";
51
+ import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
52
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
54
53
  import { isPlainObject, parseJsonOrThrow } from "@cosmicdrift/kumiko-framework/utils";
55
54
  import { escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
56
55
  import { Temporal } from "temporal-polyfill";
@@ -530,21 +529,57 @@ export async function runTailwindOnce(entry, cwd) {
530
529
  if (!canResolveTailwindStylesheet(entry, { bun: bunResolver, cwd })) {
531
530
  throw new Error(`[kumiko build] tailwindcss nicht auflösbar für ${entry} — peer dependency fehlt am Stylesheet-Ort.`);
532
531
  }
533
- const tmpDir = await mkdtemp(join(tmpdir(), "kumiko-build-tw-"));
534
- const outPath = join(tmpDir, "styles.css");
535
- const bunPath = process.argv[0] ?? "bun";
536
- const proc = Bun.spawn([bunPath, "run", cliPath, "-i", entry, "-o", outPath, "--minify"], {
537
- cwd,
538
- stdout: "inherit",
539
- stderr: "inherit",
532
+ const { compile, optimize, Scanner } = await loadTailwindEngine(cliPath, bunResolver);
533
+ const input = resolve(cwd, entry);
534
+ if (!existsSync(input)) {
535
+ throw new Error(`[kumiko build] tailwind: stylesheet not found: ${input}`);
536
+ }
537
+ const css = await readFile(input, "utf8");
538
+ // Mirrors `@tailwindcss/cli -i <entry> -o <file> --minify` option for option, so the output
539
+ // stays byte-identical to the CLI without spawning a child process.
540
+ const compiler = await compile(css, {
541
+ from: input,
542
+ base: dirname(input),
543
+ onDependency: () => { },
540
544
  });
541
- const code = await proc.exited;
542
- if (code !== 0) {
543
- throw new Error(`[kumiko build] tailwind exit ${code}`);
545
+ const rootSources = compiler.root === "none"
546
+ ? []
547
+ : compiler.root === null
548
+ ? [{ base: resolve(cwd), pattern: "**/*", negated: false }]
549
+ : [{ ...compiler.root, negated: false }];
550
+ const scanner = new Scanner({
551
+ sources: [
552
+ ...rootSources,
553
+ ...compiler.sources,
554
+ { base: dirname(process.execPath), pattern: basename(process.execPath), negated: true },
555
+ ],
556
+ });
557
+ return optimize(compiler.build(scanner.scan()), { file: input, minify: true }).code;
558
+ }
559
+ function isRecord(value) {
560
+ return typeof value === "object" && value !== null;
561
+ }
562
+ // Dynamic import of an external package: only the shape is checkable, the signatures are the documented Tailwind v4 API.
563
+ function isTailwindEngine(candidate) {
564
+ return (typeof candidate.compile === "function" &&
565
+ typeof candidate.optimize === "function" &&
566
+ typeof candidate.Scanner === "function");
567
+ }
568
+ // Both packages are the CLI's own dependencies, so they resolve from the CLI package directory
569
+ // (same version the CLI would run), not from the app root.
570
+ async function loadTailwindEngine(cliPath, bun) {
571
+ const cliDir = dirname(bun.resolveSync("@tailwindcss/cli/package.json", dirname(cliPath)));
572
+ const node = await import(bun.resolveSync("@tailwindcss/node", cliDir));
573
+ const oxide = await import(bun.resolveSync("@tailwindcss/oxide", cliDir));
574
+ const engine = {
575
+ compile: isRecord(node) ? node["compile"] : undefined,
576
+ optimize: isRecord(node) ? node["optimize"] : undefined,
577
+ Scanner: isRecord(oxide) ? oxide["Scanner"] : undefined,
578
+ };
579
+ if (!isTailwindEngine(engine)) {
580
+ throw new Error("[kumiko build] @tailwindcss/node or @tailwindcss/oxide has an unexpected API.");
544
581
  }
545
- const css = await readFile(outPath, "utf8");
546
- await rm(tmpDir, { recursive: true, force: true });
547
- return css;
582
+ return engine;
548
583
  }
549
584
  // Exact `<base>-<hash>.js` match: a plain prefix check would let
550
585
  // `client-admin-legacy-<hash>.js` satisfy the `client-admin` entry. The hash is
@@ -4,12 +4,14 @@ import type { SseBroker } from "@cosmicdrift/kumiko-framework/api";
4
4
  import type { KmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
5
5
  import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
6
6
  import type { ConfigResolver, FeatureDefinition, Registry } from "@cosmicdrift/kumiko-framework/engine";
7
+ import type { Logger } from "@cosmicdrift/kumiko-framework/logging";
7
8
  import type { MasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
8
9
  import { type BootCrypto } from "./boot/boot-crypto.js";
9
10
  import type { AuthMailOptions, EmailVerificationSetup, InviteSetup, PasswordResetSetup, SignupSetup } from "./run-prod-app.js";
10
11
  export declare function addConfigAccessorFactory<T extends {
11
12
  readonly configResolver?: ConfigResolver;
12
13
  }>(resolved: T, registry: Registry): T;
14
+ export declare function loggerFromExtraContext(extraContext: Record<string, unknown>): Logger | undefined;
13
15
  export declare function buildBootExtraContext(opts: {
14
16
  readonly db: DbConnection;
15
17
  readonly features: readonly FeatureDefinition[];
@@ -23,6 +25,8 @@ export declare function buildBootExtraContext(opts: {
23
25
  /** One-shot process (runBootstrap): nothing drains its job queue once it
24
26
  * exits, so queued channels must send inline instead of enqueueing. */
25
27
  readonly deliverQueuedInline?: boolean;
28
+ /** Receives redacted delivery failures; without it they go to the console. */
29
+ readonly log?: Logger;
26
30
  }): Record<string, unknown>;
27
31
  /** Die Auth-Felder die resolveAuthMail liest/normalisiert — beide
28
32
  * App-Auth-Typen (prod + dev) erfüllen das strukturell. */
@@ -4,7 +4,7 @@ import { resolveSessionStore } from "@cosmicdrift/kumiko-bundled-features/auth-f
4
4
  import { bindMfaRevokeAllOtherSessionsFromFeature, bindRevokeAllPatTokensFromFeature, } from "@cosmicdrift/kumiko-bundled-features/auth-mfa";
5
5
  import { createSmtpTransportFromEnv } from "@cosmicdrift/kumiko-bundled-features/channel-email";
6
6
  import { buildEnvConfigOverrides, createConfigAccessorFactory, createConfigResolver, } from "@cosmicdrift/kumiko-bundled-features/config";
7
- import { collectChannels, createDeliveryService, DELIVERY_FEATURE, } from "@cosmicdrift/kumiko-bundled-features/delivery";
7
+ import { collectChannels, createDeliveryNotifyFactory, createDeliveryService, DELIVERY_FEATURE, } from "@cosmicdrift/kumiko-bundled-features/delivery";
8
8
  import { bindPatAutoRevokeOnPasswordChangeFromFeature, revokeAllPatTokensForUser, } from "@cosmicdrift/kumiko-bundled-features/personal-access-tokens";
9
9
  import { createSecretsContext, SECRETS_FEATURE_NAME, } from "@cosmicdrift/kumiko-bundled-features/secrets";
10
10
  import { bindAutoRevokeFromFeature } from "@cosmicdrift/kumiko-bundled-features/sessions";
@@ -52,8 +52,21 @@ function buildDeliveryNotifyFactory(opts) {
52
52
  ...(opts.secrets && { secrets: opts.secrets }),
53
53
  ...(opts.sseBroker && { sseBroker: opts.sseBroker }),
54
54
  ...(opts.escapeHatchAuditSink && { escapeHatchAuditSink: opts.escapeHatchAuditSink }),
55
+ ...(opts.log && { log: opts.log }),
55
56
  });
56
- return (user, tenantId, jobDispatcher) => (notificationType, options) => deliveryService.notify(notificationType, options, user, tenantId, opts.deliverQueuedInline === true ? undefined : jobDispatcher);
57
+ return createDeliveryNotifyFactory(deliveryService, {
58
+ deliverQueuedInline: opts.deliverQueuedInline === true,
59
+ });
60
+ }
61
+ function isLogger(value) {
62
+ if (typeof value !== "object" || value === null)
63
+ return false;
64
+ return ["info", "warn", "error", "debug", "child"].every((method) => method in value && typeof Reflect.get(value, method) === "function");
65
+ }
66
+ // Prod context.log only comes from the app's extraContext.
67
+ export function loggerFromExtraContext(extraContext) {
68
+ const candidate = extraContext["log"];
69
+ return isLogger(candidate) ? candidate : undefined;
57
70
  }
58
71
  function resolveEscapeHatchAuditSink(features, db) {
59
72
  const hasAuditFeature = features.some((f) => f.name === AUDIT_FEATURE);
@@ -86,6 +99,7 @@ export function buildBootExtraContext(opts) {
86
99
  ...(secrets && { secrets }),
87
100
  ...(opts.sseBroker && { sseBroker: opts.sseBroker }),
88
101
  ...(escapeHatchAuditSink && { escapeHatchAuditSink }),
102
+ ...(opts.log && { log: opts.log }),
89
103
  deliverQueuedInline: opts.deliverQueuedInline,
90
104
  }),
91
105
  }),
@@ -6,6 +6,7 @@ import { type KmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
6
6
  import { type EffectiveFeaturesResolver, type FeatureDefinition, type ValidateBootOptions } from "@cosmicdrift/kumiko-framework/engine";
7
7
  import { type AllInOneEntrypoint, type ApiEntrypoint } from "@cosmicdrift/kumiko-framework/entrypoint";
8
8
  import { type ComposedEnvSchema, KumikoBootError } from "@cosmicdrift/kumiko-framework/env";
9
+ import type { ErrorDocsConfig } from "@cosmicdrift/kumiko-framework/errors";
9
10
  import type { JobRunnerOptions } from "@cosmicdrift/kumiko-framework/jobs";
10
11
  import type { ObservabilityOptions, ObservabilityProvider } from "@cosmicdrift/kumiko-framework/observability";
11
12
  import type { MasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
@@ -14,7 +15,7 @@ import { Redis } from "ioredis";
14
15
  import { type SystemWireDeps } from "./extra-routes-deps.js";
15
16
  import { type SecurityHeadersOption } from "./security-headers.js";
16
17
  export { buildBunServeOptions } from "./bun-serve-options.js";
17
- export { addConfigAccessorFactory, buildBootExtraContext, resolveAuthMail, } from "./run-prod-app-boot-context.js";
18
+ export { addConfigAccessorFactory, buildBootExtraContext, loggerFromExtraContext, resolveAuthMail, } from "./run-prod-app-boot-context.js";
18
19
  export { staticCachePolicy } from "./run-prod-app-static-files.js";
19
20
  export declare function requireEnv(name: string, src?: Record<string, string | undefined>, context?: string): string;
20
21
  /** Wrapper API for the password-reset flow.
@@ -156,9 +157,13 @@ export type RunProdAppAuthOptions = {
156
157
  };
157
158
  /** Hook for app-specific seeding — runs after the admin (when auth is
158
159
  * active). Each seed is responsible for its own idempotence (seeds are
159
- * expected to check "is my row already there?" before inserting). */
160
+ * expected to check "is my row already there?" before inserting). Write
161
+ * through `dispatcher.write(...)` rather than `db` so projections and
162
+ * hooks run; `registry` is the boot registry. */
160
163
  export type ProdSeedFn = (deps: {
161
164
  db: import("@cosmicdrift/kumiko-framework/db").DbConnection;
165
+ registry: import("@cosmicdrift/kumiko-framework/engine").Registry;
166
+ dispatcher: import("@cosmicdrift/kumiko-framework/pipeline").Dispatcher;
162
167
  }) => Promise<void>;
163
168
  /** Boot-Time-Deps die `extraContext` + `anonymousAccess` Factories als
164
169
  * Argument bekommen. Closure dann in der returned Config (z.B. ein
@@ -306,10 +311,11 @@ export type RunProdAppOptions = {
306
311
  * no locale signal. Merged into AppContext before extraContext (app wins). */
307
312
  readonly defaultLocale?: string;
308
313
  readonly extraContext?: ExtraContextOption;
309
- /** MasterKeyProvider für die auto-verdrahtete `ctx.secrets`. Default:
310
- * `createEnvMasterKeyProvider` (KEK aus `KUMIKO_SECRETS_MASTER_KEY_V<n>`).
311
- * Override für KMS-Backends (AWS/GCP/Azure) statt env-KEK. Nur relevant
312
- * wenn das `secrets`-Feature gemountet ist. */
314
+ /** MasterKeyProvider replacing the env KEK (`KUMIKO_SECRETS_MASTER_KEY_V<n>`)
315
+ * for `ctx.secrets`, encrypted config keys and `encrypted: true` entity
316
+ * fields. Default: `createEnvMasterKeyProvider`. Use it for KMS backends
317
+ * (AWS/GCP/Azure); with it set, `KUMIKO_SECRETS_MASTER_KEY_V1` is not
318
+ * required in the env. */
313
319
  readonly masterKey?: MasterKeyProvider;
314
320
  /** Subject-Key-Adapter für Crypto-Shredding (DSGVO Art. 17). Wenn gesetzt,
315
321
  * steht er Feature-Code als `ctx.kms` zur Verfügung und der Boot prüft
@@ -447,6 +453,8 @@ export type RunProdAppOptions = {
447
453
  * `auth.trustedProxyHops` and `KUMIKO_TRUSTED_PROXY_HOPS`. Default 0 =
448
454
  * trust no proxy header, only the socket address (or "unknown") counts. */
449
455
  readonly trustedProxyHops?: number;
456
+ /** Links the app's own error reasons to its own docs (`docsUrl` in error responses); framework reasons keep the framework docs link. */
457
+ readonly errorDocs?: ErrorDocsConfig;
450
458
  };
451
459
  export type ProdAppHandle = {
452
460
  /** The composed entrypoint — AllInOne (runSingleInstance, default) or
@@ -44,6 +44,7 @@ import { seedAdminGuarded, } from "@cosmicdrift/kumiko-bundled-features/auth-ema
44
44
  import { EXT_SESSION_STORE, EXT_TOKEN_VERIFIER, resolveAnonymousAccessFromRegistry, resolveTokenVerifier, } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
45
45
  import { AUTH_MFA_FEATURE, AuthMfaHandlers } from "@cosmicdrift/kumiko-bundled-features/auth-mfa";
46
46
  import { PAT_FEATURE, patRateLimitFromFeature, } from "@cosmicdrift/kumiko-bundled-features/personal-access-tokens";
47
+ import { SECRETS_MASTER_KEK_ENV_KEYS } from "@cosmicdrift/kumiko-bundled-features/secrets";
47
48
  import { SESSIONS_FEATURE } from "@cosmicdrift/kumiko-bundled-features/sessions";
48
49
  import { TenantQueries } from "@cosmicdrift/kumiko-bundled-features/tenant";
49
50
  import { UserQueries } from "@cosmicdrift/kumiko-bundled-features/user";
@@ -52,7 +53,7 @@ import { configureBlindIndexKey, configurePiiSubjectKms, resolvePlatformKeks, }
52
53
  import { configureEntityFieldEncryption, createDbConnection, } from "@cosmicdrift/kumiko-framework/db";
53
54
  import { collectWriteHandlerQns, createRegistry, findTierResolverUsage, validateAppCustomScreenWriteQns, validateBoot, } from "@cosmicdrift/kumiko-framework/engine";
54
55
  import { createAllInOneEntrypoint, createApiEntrypoint, } from "@cosmicdrift/kumiko-framework/entrypoint";
55
- import { KumikoBootError, kmsSlotsOf, parseEnv, } from "@cosmicdrift/kumiko-framework/env";
56
+ import { KumikoBootError, kmsSlotsOf, parseEnv, withOptionalEnvKeys, } from "@cosmicdrift/kumiko-framework/env";
56
57
  import { renderDryRun } from "@cosmicdrift/kumiko-framework/env/dry-run";
57
58
  import { createEsOperationsTable, createSeedMigrationContext, runPendingSeedMigrations, } from "@cosmicdrift/kumiko-framework/es-ops";
58
59
  import { assertKumikoSchemaCurrent, SchemaDriftError, } from "@cosmicdrift/kumiko-framework/migrations";
@@ -68,14 +69,14 @@ import { buildBunServeOptions, resolveDerivedMaxRequestBodySize } from "./bun-se
68
69
  import { buildComposeAuthOptions, composeFeatures } from "./compose-features.js";
69
70
  import { makeDispatchSystemWrite } from "./extra-routes-deps.js";
70
71
  import { assertPiiBootInvariants } from "./pii-boot-gate.js";
71
- import { addConfigAccessorFactory, buildBootExtraContext, buildProdSessionAuth, resolveAuthMail, wireProdPatAutoRevoke, } from "./run-prod-app-boot-context.js";
72
+ import { addConfigAccessorFactory, buildBootExtraContext, buildProdSessionAuth, loggerFromExtraContext, resolveAuthMail, wireProdPatAutoRevoke, } from "./run-prod-app-boot-context.js";
72
73
  import { buildStaticFallback } from "./run-prod-app-static-files.js";
73
74
  import { withSecurityHeaders } from "./security-headers.js";
74
75
  import { assertSessionBootInvariants } from "./session-boot-gate.js";
75
76
  import { shouldWireProdSessions } from "./session-wiring.js";
76
77
  import { stripNoRouteMatchHeader } from "./try-hono-first.js";
77
78
  export { buildBunServeOptions } from "./bun-serve-options.js";
78
- export { addConfigAccessorFactory, buildBootExtraContext, resolveAuthMail, } from "./run-prod-app-boot-context.js";
79
+ export { addConfigAccessorFactory, buildBootExtraContext, loggerFromExtraContext, resolveAuthMail, } from "./run-prod-app-boot-context.js";
79
80
  export { staticCachePolicy } from "./run-prod-app-static-files.js";
80
81
  // Strict env-var read. Throws with a clear hint when missing — better
81
82
  // than discovering a Postgres-connection-refused 30s into the boot.
@@ -191,7 +192,10 @@ export async function runProdApp(options) {
191
192
  // a real env-check (all required vars present + schema-valid) before
192
193
  // it asserts feature-wiring works.
193
194
  try {
194
- parseEnv(options.envSchema.schema, rawEnvSource, {
195
+ const envSchema = options.masterKey
196
+ ? withOptionalEnvKeys(options.envSchema.schema, SECRETS_MASTER_KEK_ENV_KEYS)
197
+ : options.envSchema.schema;
198
+ parseEnv(envSchema, rawEnvSource, {
195
199
  sources: options.envSchema.sources,
196
200
  ...(options.pulumiPrefix ? { pulumiPrefix: options.pulumiPrefix } : {}),
197
201
  });
@@ -269,7 +273,12 @@ export async function runProdApp(options) {
269
273
  includeBundled: !!effectiveAuth,
270
274
  ...(composeAuthOptions && { authOptions: composeAuthOptions }),
271
275
  });
272
- validateBoot(features, { env: envSource, ...options.validateBootOptions });
276
+ const bootCrypto = resolveBootCrypto(envSource, options.masterKey);
277
+ validateBoot(features, {
278
+ env: envSource,
279
+ ...(bootCrypto.entityFieldCipher && { entityFieldCipher: bootCrypto.entityFieldCipher }),
280
+ ...options.validateBootOptions,
281
+ });
273
282
  warnIfNonUtcServerTimeZone();
274
283
  validateAppCustomScreenWriteQns(process.cwd(), collectWriteHandlerQns(features));
275
284
  assertPiiBootInvariants(features, {
@@ -385,7 +394,6 @@ export async function runProdApp(options) {
385
394
  : (options.extraContext ?? {});
386
395
  // Framework-Default-Provider zuerst, App-Werte (resolvedExtraContext)
387
396
  // gewinnen immer (z.B. money-horse's eigener configResolver).
388
- const bootCrypto = resolveBootCrypto(envSource, options.masterKey);
389
397
  // App-wide cipher for `encrypted: true` entity fields — executors resolve
390
398
  // it lazily, entities without encrypted fields never touch it.
391
399
  configureEntityFieldEncryption(bootCrypto.entityFieldCipher);
@@ -399,6 +407,7 @@ export async function runProdApp(options) {
399
407
  // jeder Equality-Lookup (Login by email!) liefe ins Leere. Fail-fast statt
400
408
  // silent-broken-auth.
401
409
  configureBlindIndexKey(options.blindIndexKey);
410
+ const bootLogger = loggerFromExtraContext(resolvedExtraContext);
402
411
  const autoExtraContext = buildBootExtraContext({
403
412
  db,
404
413
  features,
@@ -408,6 +417,7 @@ export async function runProdApp(options) {
408
417
  sseBroker,
409
418
  crypto: bootCrypto,
410
419
  ...(options.kms && { kms: options.kms }),
420
+ ...(bootLogger && { log: bootLogger }),
411
421
  });
412
422
  const extraContext = {
413
423
  ...addConfigAccessorFactory({ ...autoExtraContext, ...resolvedExtraContext }, registry),
@@ -483,6 +493,7 @@ export async function runProdApp(options) {
483
493
  ...(options.metrics && { metrics: options.metrics }),
484
494
  ...(options.rateLimit && { rateLimit: options.rateLimit }),
485
495
  ...(trustedProxyHops !== undefined && { trustedProxyHops }),
496
+ ...(options.errorDocs && { errorDocs: options.errorDocs }),
486
497
  ...(options.extraRoutes && { extraRoutes: options.extraRoutes }),
487
498
  ...(effectiveAuth && {
488
499
  auth: {
@@ -643,23 +654,21 @@ export async function runProdApp(options) {
643
654
  db,
644
655
  ...(bootCrypto.configCipher && { cipher: bootCrypto.configCipher }),
645
656
  });
646
- for (const seed of options.seeds ?? []) {
647
- await seed({ db });
657
+ const seedDispatcher = (options.seeds?.length ?? 0) > 0 || options.seedsDir !== undefined
658
+ ? createDispatcher(registry, { db, redis, entityCache, registry, ...extraContext })
659
+ : undefined;
660
+ if (seedDispatcher) {
661
+ for (const seed of options.seeds ?? []) {
662
+ await seed({ db, registry, dispatcher: seedDispatcher });
663
+ }
648
664
  }
649
665
  // ES-Operations / Seed-Migrations (Phase 1). Läuft NACH applyBootSeeds +
650
666
  // existing seeds-array — die deklarativen Seeds sind die "always-insert-
651
667
  // if-missing"-Schicht; seed-migrations sind die "diff-and-update"-
652
668
  // Schicht für Drift den existing Seeds nicht erfassen können (z.B.
653
669
  // Membership-Roles-Change nach initialer Seed-Erstellung).
654
- if (options.seedsDir !== undefined && envSource["KUMIKO_SKIP_ES_OPS"] !== "1") {
670
+ if (seedDispatcher && options.seedsDir !== undefined && envSource["KUMIKO_SKIP_ES_OPS"] !== "1") {
655
671
  await createEsOperationsTable(db);
656
- const seedDispatcher = createDispatcher(registry, {
657
- db,
658
- redis,
659
- entityCache,
660
- registry,
661
- ...extraContext,
662
- });
663
672
  await runPendingSeedMigrations({
664
673
  db,
665
674
  seedsDir: options.seedsDir,
@@ -49,7 +49,9 @@ export type RunWorkerAppOptions = {
49
49
  * RunProdAppOptions["extraContext"], without sseBroker (the worker
50
50
  * has none — see entrypoint/index.ts's documented SSE limitation). */
51
51
  readonly extraContext?: WorkerContextOption;
52
- /** MasterKeyProvider for ctx.secrets. Default: env-KEK (see
52
+ /** MasterKeyProvider replacing the env KEK for ctx.secrets, encrypted
53
+ * config keys and `encrypted: true` entity fields; with it set,
54
+ * `KUMIKO_SECRETS_MASTER_KEY_V1` is not required (see
53
55
  * RunProdAppOptions["masterKey"]). */
54
56
  readonly masterKey?: MasterKeyProvider;
55
57
  /** Subject-key adapter for crypto-shredding — boot checks health()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-server-runtime",
3
- "version": "0.342.0",
3
+ "version": "0.344.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.342.0",
84
- "@cosmicdrift/kumiko-framework": "0.342.0",
85
- "@cosmicdrift/kumiko-headless": "0.342.0",
86
- "@cosmicdrift/kumiko-renderer-web": "0.342.0",
83
+ "@cosmicdrift/kumiko-bundled-features": "0.344.0",
84
+ "@cosmicdrift/kumiko-framework": "0.344.0",
85
+ "@cosmicdrift/kumiko-headless": "0.344.0",
86
+ "@cosmicdrift/kumiko-renderer-web": "0.344.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,29 @@
1
1
  [
2
+ {
3
+ "version": "0.343.0",
4
+ "type": "fix",
5
+ "title": "Delivery failures are logged to the app logger in prod",
6
+ "detail": "The prod and worker boot pass the app logger (`extraContext.log`) to the delivery service, so redacted delivery failures reach that logger instead of the console. `ctx.notify` is built with `createDeliveryNotifyFactory`. `buildBootExtraContext` accepts an optional `log`."
7
+ },
8
+ {
9
+ "version": "0.343.0",
10
+ "type": "improvement",
11
+ "title": "masterKey on runProdApp and runWorkerApp replaces the env KEK for the boot checks",
12
+ "detail": "A `masterKey` provider on runProdApp and runWorkerApp is enough for the boot checks\nThe boot probe for `encrypted: true` entity fields now accepts the cipher the runner builds from `masterKey` (new `ValidateBootOptions.entityFieldCipher`), and the env-schema parse no longer requires `KUMIKO_SECRETS_MASTER_KEY_V1` when `masterKey` is set. The framework exports `withOptionalEnvKeys` for that, and the secrets feature exports `SECRETS_MASTER_KEK_ENV_KEYS`. Without `masterKey`, a missing env KEK still stops the boot; the message now names both ways to provide a key. A malformed env KEK now fails during boot validation, so `KUMIKO_DRY_RUN_ENV=boot` reports it too.",
13
+ "migration": "Apps that pass `masterKey` can drop `KUMIKO_SECRETS_MASTER_KEY_V1` from their env and deployment config. Apps without `masterKey` change nothing."
14
+ },
15
+ {
16
+ "version": "0.343.0",
17
+ "type": "improvement",
18
+ "title": "Prod seeds get registry and dispatcher",
19
+ "detail": "Prod seeds receive the boot registry and a system dispatcher\n`ProdSeedFn` deps grow from `{ db }` to `{ db, registry, dispatcher }`. Seeds should write through `dispatcher.write(...)` so projections and hooks run. The dispatcher is built once when `seeds` or `seedsDir` is set and is shared with the seed migrations."
20
+ },
21
+ {
22
+ "version": "0.343.0",
23
+ "type": "improvement",
24
+ "title": "Prod build runs Tailwind in-process instead of spawning the CLI",
25
+ "detail": "The prod build runs Tailwind in-process (`@tailwindcss/node` and `@tailwindcss/oxide`, resolved from the `@tailwindcss/cli` package) instead of spawning the CLI. Options mirror the CLI, and a parity test pins byte-identical output."
26
+ },
2
27
  {
3
28
  "version": "0.341.0",
4
29
  "type": "improvement",