@cosmicdrift/kumiko-server-runtime 0.374.0 → 0.377.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.
Files changed (35) hide show
  1. package/dist/boot/env-gate.d.ts +23 -0
  2. package/dist/boot/env-gate.js +136 -0
  3. package/dist/boot/worker-boot-core.d.ts +3 -0
  4. package/dist/boot/worker-boot-core.js +10 -2
  5. package/dist/boot-options/demo-mode.d.ts +7 -1
  6. package/dist/boot-options/demo-mode.js +5 -2
  7. package/dist/boot-options/feature-toggles-option.d.ts +19 -0
  8. package/dist/boot-options/feature-toggles-option.js +25 -0
  9. package/dist/boot-options/file-storage-option.d.ts +11 -2
  10. package/dist/boot-options/file-storage-option.js +48 -9
  11. package/dist/boot-options/mail-option.d.ts +8 -3
  12. package/dist/boot-options/mail-option.js +21 -6
  13. package/dist/boot-options/option-env-schema.js +30 -9
  14. package/dist/boot-options/resolve-boot-options.d.ts +9 -1
  15. package/dist/boot-options/resolve-boot-options.js +37 -4
  16. package/dist/boot-options/stack-boot-requirements.d.ts +5 -2
  17. package/dist/boot-options/stack-boot-requirements.js +23 -8
  18. package/dist/build-prod-bundle.d.ts +3 -3
  19. package/dist/build-prod-bundle.js +5 -5
  20. package/dist/extra-routes-deps.d.ts +5 -0
  21. package/dist/index.d.ts +5 -2
  22. package/dist/index.js +3 -0
  23. package/dist/renderer-web-fonts.js +1 -1
  24. package/dist/request-bound-system-query.d.ts +1 -1
  25. package/dist/resolve-tailwind-cli.d.ts +1 -1
  26. package/dist/resolve-tailwind-cli.js +1 -1
  27. package/dist/run-prod-app-boot-context.d.ts +4 -0
  28. package/dist/run-prod-app-boot-context.js +12 -5
  29. package/dist/run-prod-app-static-files.js +1 -1
  30. package/dist/run-prod-app.d.ts +22 -12
  31. package/dist/run-prod-app.js +45 -144
  32. package/dist/run-worker-app.d.ts +23 -4
  33. package/dist/run-worker-app.js +51 -5
  34. package/package.json +7 -8
  35. package/src/changes.json +51 -0
@@ -1,13 +1,16 @@
1
1
  import { BILLING_FOUNDATION_FEATURE } from "@cosmicdrift/kumiko-bundled-features/billing-foundation";
2
2
  import { DELIVERY_FEATURE } from "@cosmicdrift/kumiko-bundled-features/delivery";
3
+ import { FEATURE_TOGGLES_FEATURE } from "@cosmicdrift/kumiko-bundled-features/feature-toggles";
3
4
  import { SUBSCRIPTION_STRIPE_FEATURE } from "@cosmicdrift/kumiko-bundled-features/subscription-stripe";
4
5
  import { TIER_ENGINE_FEATURE } from "@cosmicdrift/kumiko-bundled-features/tier-engine";
5
6
  import { resolveBillingOption } from "./billing-option.js";
6
7
  import { buildDemoModeRoute, withDemoReadOnlyFetch } from "./demo-mode.js";
8
+ import { resolveFeatureTogglesOption, } from "./feature-toggles-option.js";
7
9
  import { FILE_FOUNDATION_FEATURE, resolveFileStorageOption, } from "./file-storage-option.js";
8
10
  import { resolveKmsMode } from "./kms-mode.js";
9
11
  import { CHANNEL_EMAIL_FEATURE, resolveMailOption } from "./mail-option.js";
10
12
  import { resolveSeedShortcuts } from "./seed-shortcuts.js";
13
+ import { stackBootWarnings } from "./stack-boot-requirements.js";
11
14
  export function isSeedFnList(seeds) {
12
15
  return Array.isArray(seeds);
13
16
  }
@@ -22,6 +25,35 @@ function rejectDoubleMount(mounted, option, feature) {
22
25
  throw new Error(`[runProdApp] ${option} mounts ${feature} itself — remove it from \`features\` or drop the option.`);
23
26
  }
24
27
  }
28
+ // The old option was a function the app computed; ignoring it silently would drop an operator kill-switch.
29
+ export function rejectRemovedEffectiveFeatures(options) {
30
+ if ("effectiveFeatures" in options) {
31
+ throw new Error("[runProdApp] The `effectiveFeatures` option was removed — operator switches no longer take effect through it. Set `featureToggles: true` (it mounts feature-toggles and layers the switches over the tier resolver) and drop the `feature-toggles` feature from `features`.");
32
+ }
33
+ }
34
+ function wireFeatureToggles(options, mounted) {
35
+ rejectRemovedEffectiveFeatures(options);
36
+ if (options.featureToggles) {
37
+ rejectDoubleMount(mounted, "featureToggles", FEATURE_TOGGLES_FEATURE);
38
+ return resolveFeatureTogglesOption();
39
+ }
40
+ if (mounted.has(FEATURE_TOGGLES_FEATURE)) {
41
+ throw new Error(`[runProdApp] ${FEATURE_TOGGLES_FEATURE} is in \`features\` but \`featureToggles\` is not set — its switches would not reach the dispatcher. Set \`featureToggles: true\` and remove the feature from \`features\`.`);
42
+ }
43
+ return undefined;
44
+ }
45
+ function warnAboutBootOptions(options) {
46
+ const bootWarnings = stackBootWarnings(options).map((line) => `stacks in \`features\` expect boot options that are not set:\n${line}`);
47
+ if (options.runSingleInstance === false &&
48
+ (options.mail || options.fileStorage || options.featureToggles) &&
49
+ options.workerMountsFeatures !== true) {
50
+ bootWarnings.push("runSingleInstance:false — delivery and file jobs run in runWorkerApp. Pass the same mail / fileStorage / featureToggles options there, or set workerMountsFeatures: true if it mounts those features itself.");
51
+ }
52
+ if (bootWarnings.length > 0) {
53
+ // biome-ignore lint/suspicious/noConsole: boot-time warn, no logger configured this early
54
+ console.warn(`[runProdApp] ${bootWarnings.join("\n[runProdApp] ")}`);
55
+ }
56
+ }
25
57
  /** Runs after the env is validated and KEK slots are decrypted, before any
26
58
  * connection: every misconfiguration of the new options fails here. */
27
59
  export async function resolveBootOptionWiring(options, envSource) {
@@ -56,14 +88,14 @@ export async function resolveBootOptionWiring(options, envSource) {
56
88
  features.push(billing.feature);
57
89
  extraRoutes.push(billing.route);
58
90
  }
91
+ const featureToggles = wireFeatureToggles(options, mounted);
92
+ if (featureToggles)
93
+ features.push(featureToggles.feature);
59
94
  const { demoMode } = options;
60
95
  if (demoMode) {
61
96
  extraRoutes.push(buildDemoModeRoute(demoMode, envSource));
62
97
  }
63
- if (options.runSingleInstance === false && (options.mail || options.fileStorage)) {
64
- // biome-ignore lint/suspicious/noConsole: boot-time warn, no logger configured this early
65
- console.warn("[runProdApp] runSingleInstance:false — delivery and file jobs run in runWorkerApp, which does not read mail / fileStorage. Mount the same channel-email and file provider features there.");
66
- }
98
+ warnAboutBootOptions(options);
67
99
  const seeds = options.seeds ?? [];
68
100
  const shortcutSeeds = isSeedFnList(seeds)
69
101
  ? []
@@ -80,6 +112,7 @@ export async function resolveBootOptionWiring(options, envSource) {
80
112
  extraRoutes,
81
113
  envDefaults,
82
114
  kms,
115
+ ...(featureToggles && { featureToggles }),
83
116
  shortcutSeeds,
84
117
  customSeeds,
85
118
  mailConfigured: options.mail !== undefined,
@@ -2,5 +2,8 @@ import { type BootOptionName } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import type { RunProdAppOptions } from "../run-prod-app.ts";
3
3
  export type StackBootOptionFields = Pick<RunProdAppOptions, "features" | "kms" | "allowPlaintextPii" | BootOptionName>;
4
4
  /** Reads `bootRequires` from the raw feature list: dedupe may later drop the
5
- * tagged copy of a feature two stacks share. */
6
- export declare function assertStackBootRequirements(options: StackBootOptionFields): void;
5
+ * tagged copy of a feature two stacks share. `scope` limits the check to the
6
+ * options the calling boot function can set at all (runDevApp has no `signals`). */
7
+ export declare function assertStackBootRequirements(options: StackBootOptionFields, scope?: readonly BootOptionName[]): void;
8
+ /** Requirements a stack marked `severity: "warn"`: logged at boot, never fatal. */
9
+ export declare function stackBootWarnings(options: StackBootOptionFields): string[];
@@ -25,6 +25,10 @@ const OPTION_CHECKS = {
25
25
  isSatisfied: (options, mounted) => options.billing !== undefined || mountsExtension(mounted, SUBSCRIPTION_PROVIDER_EXTENSION),
26
26
  hint: "set billing: { stripe: { priceToTier }, defaultTier }",
27
27
  },
28
+ featureToggles: {
29
+ isSatisfied: (options) => options.featureToggles === true,
30
+ hint: "set featureToggles: true",
31
+ },
28
32
  demoMode: {
29
33
  isSatisfied: (options) => options.demoMode !== undefined,
30
34
  hint: "set demoMode: { readOnly }",
@@ -42,22 +46,33 @@ const OPTION_CHECKS = {
42
46
  hint: "drop rateLimit: false",
43
47
  },
44
48
  };
45
- /** Reads `bootRequires` from the raw feature list: dedupe may later drop the
46
- * tagged copy of a feature two stacks share. */
47
- export function assertStackBootRequirements(options) {
48
- const gaps = new Set();
49
+ function unsatisfiedRequirements(options, severity, scope) {
50
+ const lines = new Set();
49
51
  for (const feature of options.features) {
50
52
  const requirement = feature.bootRequires;
51
- if (requirement === undefined)
53
+ if (requirement === undefined || (requirement.severity ?? "error") !== severity)
52
54
  continue;
53
55
  for (const option of requirement.options) {
56
+ if (scope !== undefined && !scope.includes(option))
57
+ continue;
54
58
  const check = OPTION_CHECKS[option];
55
59
  if (!check.isSatisfied(options, options.features)) {
56
- gaps.add(` - ${requirement.stack} needs the boot option \`${option}\`: ${check.hint}`);
60
+ lines.add(` - ${requirement.stack} needs the boot option \`${option}\`: ${check.hint}`);
57
61
  }
58
62
  }
59
63
  }
60
- if (gaps.size > 0) {
61
- throw new Error(`[runProdApp] stacks in \`features\` need boot options that are not set:\n${[...gaps].join("\n")}`);
64
+ return [...lines];
65
+ }
66
+ /** Reads `bootRequires` from the raw feature list: dedupe may later drop the
67
+ * tagged copy of a feature two stacks share. `scope` limits the check to the
68
+ * options the calling boot function can set at all (runDevApp has no `signals`). */
69
+ export function assertStackBootRequirements(options, scope) {
70
+ const gaps = unsatisfiedRequirements(options, "error", scope);
71
+ if (gaps.length > 0) {
72
+ throw new Error(`[runProdApp] stacks in \`features\` need boot options that are not set:\n${gaps.join("\n")}`);
62
73
  }
63
74
  }
75
+ /** Requirements a stack marked `severity: "warn"`: logged at boot, never fatal. */
76
+ export function stackBootWarnings(options) {
77
+ return unsatisfiedRequirements(options, "warn");
78
+ }
@@ -4,9 +4,9 @@ export type BuildProdBundleOptions = {
4
4
  readonly cwd?: string;
5
5
  /** Output-Folder relativ zu cwd. Default: "dist". */
6
6
  readonly outDir?: string;
7
- /** Stylesheet-Override. Default: erst src/styles.css, dann
8
- * @cosmicdrift/kumiko-renderer-web/styles.css wenn clientEntry da ist.
9
- * `false` deaktiviert die CSS-Pipeline explizit. */
7
+ /** Stylesheet override. Default: src/styles.css first, then
8
+ * @cosmicdrift/kumiko-renderer/web/styles.css when a clientEntry exists.
9
+ * `false` disables the CSS pipeline explicitly. */
10
10
  readonly stylesheet?: string | false;
11
11
  } & ClientEntriesConfig;
12
12
  /** One declared client entry, read from package.json `kumiko.clientEntries`.
@@ -14,7 +14,7 @@
14
14
  // Convention for everything else (all optional, missing → skipped):
15
15
  //
16
16
  // src/styles.css → Tailwind one-shot
17
- // (oder fallback auf @cosmicdrift/kumiko-renderer-web/styles.css
17
+ // (oder fallback auf @cosmicdrift/kumiko-renderer/web/styles.css
18
18
  // wenn nur clientEntry da ist und kein eigenes CSS)
19
19
  // public/ → rsync 1:1 (kein Hash — User-bewusste URLs)
20
20
  // public/index.html | index.html → Template, Placeholder-Tags ersetzt:
@@ -60,7 +60,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
60
60
  import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
61
61
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
62
62
  import { isPlainObject, parseJsonOrThrow } from "@cosmicdrift/kumiko-framework/utils";
63
- import { escapeHtmlAttr } from "@cosmicdrift/kumiko-headless";
63
+ import { escapeHtmlAttr } from "@cosmicdrift/kumiko-renderer/headless";
64
64
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
65
65
  import { BUNDLED_ASSETS_DIST_DIR, readBundledAssetDeclarations, resolveBundledAssetSource, } from "./bundled-assets.js";
66
66
  import { CSP_HASHES_FILE, inlineScriptHashes } from "./csp-hashes.js";
@@ -458,7 +458,7 @@ function resolveStylesheetEntry(cwd, clientEntry, override) {
458
458
  if (!hasBun)
459
459
  return undefined;
460
460
  try {
461
- const resolved = Bun.resolveSync("@cosmicdrift/kumiko-renderer-web/styles.css", cwd);
461
+ const resolved = Bun.resolveSync("@cosmicdrift/kumiko-renderer/web/styles.css", cwd);
462
462
  if (!canResolveTailwindStylesheet(resolved, { bun: Bun, cwd })) {
463
463
  return undefined;
464
464
  }
@@ -483,8 +483,8 @@ export function assertRendererWebShellPresent(css, stylesheet) {
483
483
  return;
484
484
  throw new Error(`[kumiko build] renderer-web-Fallback-CSS ohne Shell-Klasse "${RENDERER_WEB_SHELL_SENTINEL}" — ` +
485
485
  "die DefaultAppShell/WorkspaceShell-Styles fehlen, prod würde unstyled rendern. Meist ist " +
486
- "@cosmicdrift/kumiko-renderer-web nicht dort installiert wo Tailwind seine Source scannt. " +
487
- 'Fix: src/styles.css anlegen mit `@import "@cosmicdrift/kumiko-renderer-web/styles.css";` ' +
486
+ "@cosmicdrift/kumiko-renderer/web nicht dort installiert wo Tailwind seine Source scannt. " +
487
+ 'Fix: src/styles.css anlegen mit `@import "@cosmicdrift/kumiko-renderer/web/styles.css";` ' +
488
488
  '(+ `@source "./**/*.{ts,tsx}";` um auch eigene Komponenten zu scannen).');
489
489
  }
490
490
  // The compiled CSS points at absolute /assets/kumiko/fonts/<file>.woff2 URLs
@@ -2,6 +2,7 @@ import type { SystemDispatchArgs } from "@cosmicdrift/kumiko-framework/api";
2
2
  import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
3
3
  import type { Registry, WriteResult } from "@cosmicdrift/kumiko-framework/engine";
4
4
  import type { MetricsHandle } from "@cosmicdrift/kumiko-framework/observability";
5
+ import type { EnvelopeCipher, MasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
5
6
  import type { Redis } from "ioredis";
6
7
  export type { SystemDispatchArgs } from "@cosmicdrift/kumiko-framework/api";
7
8
  export { makeDispatchSystemQuery, makeDispatchSystemWrite, } from "@cosmicdrift/kumiko-framework/api";
@@ -17,6 +18,10 @@ export type SystemWireDeps = {
17
18
  /** Feature-registry — e.g. for plugin-lookups via
18
19
  * `registry.getExtensionUsages("subscriptionProvider")`. */
19
20
  readonly registry: Registry;
21
+ /** The boot's master key keyring; absent when no `KUMIKO_SECRETS_MASTER_KEY_V<n>` is set. */
22
+ readonly masterKeyProvider?: MasterKeyProvider;
23
+ /** Cipher for `encrypted: true` config keys, same instance and DEK cache as the handlers use. */
24
+ readonly configEncryption?: EnvelopeCipher;
20
25
  /** Same contract as `ctx.metricsFor`: unregistered metric names no-op. */
21
26
  readonly metricsFor: (featureName: string) => MetricsHandle;
22
27
  /** Writes through the /api/*-command-dispatcher (same idempotency/job
package/dist/index.d.ts CHANGED
@@ -1,11 +1,14 @@
1
1
  export { type BillingBootOption, resolveBillingOption } from "./boot-options/billing-option.ts";
2
- export type { DemoAccountRole, DemoModeNotice, DemoModeOption, } from "./boot-options/demo-mode.ts";
2
+ export type { DemoAccountEntry, DemoAccountRole, DemoModeNotice, DemoModeOption, } from "./boot-options/demo-mode.ts";
3
3
  export { buildDemoModeRoute, withDemoReadOnlyFetch, } from "./boot-options/demo-mode.ts";
4
4
  export type { FileStorageBootOption, FileStorageProviderName, } from "./boot-options/file-storage-option.ts";
5
5
  export type { KmsMode } from "./boot-options/kms-mode.ts";
6
- export type { MailBootOption } from "./boot-options/mail-option.ts";
6
+ export type { MailBootOption, MailRecipientScope } from "./boot-options/mail-option.ts";
7
+ export { withBootOptionEnvBlocks } from "./boot-options/option-env-schema.ts";
7
8
  export type { SignalsOption } from "./boot-options/process-signals.ts";
9
+ export { type BootOptionFields, type BootOptionWiring, isSeedFnList, rejectRemovedEffectiveFeatures, resolveBootOptionWiring, } from "./boot-options/resolve-boot-options.ts";
8
10
  export type { ProdSeedsConfig, ProdSeedTenant } from "./boot-options/seed-shortcuts.ts";
11
+ export { assertStackBootRequirements } from "./boot-options/stack-boot-requirements.ts";
9
12
  export { type BundledAssetDeclaration, type BundledAssetOptions, readBundledAsset, resolveBundledAsset, } from "./bundled-assets.ts";
10
13
  export { type ComposeFeaturesOptions, composeFeatures } from "./compose-features.ts";
11
14
  export type { RateLimitOption } from "./rate-limit-defaults.ts";
package/dist/index.js CHANGED
@@ -4,6 +4,9 @@
4
4
  // Dev-Tooling mehr in ihre node_modules.
5
5
  export { resolveBillingOption } from "./boot-options/billing-option.js";
6
6
  export { buildDemoModeRoute, withDemoReadOnlyFetch, } from "./boot-options/demo-mode.js";
7
+ export { withBootOptionEnvBlocks } from "./boot-options/option-env-schema.js";
8
+ export { isSeedFnList, rejectRemovedEffectiveFeatures, resolveBootOptionWiring, } from "./boot-options/resolve-boot-options.js";
9
+ export { assertStackBootRequirements } from "./boot-options/stack-boot-requirements.js";
7
10
  export { readBundledAsset, resolveBundledAsset, } from "./bundled-assets.js";
8
11
  export { composeFeatures } from "./compose-features.js";
9
12
  export { runBootstrap } from "./run-bootstrap.js";
@@ -17,7 +17,7 @@ export function resolveRendererWebFontsDir(searchFrom) {
17
17
  return undefined;
18
18
  for (const from of searchFrom) {
19
19
  try {
20
- const stylesheet = Bun.resolveSync("@cosmicdrift/kumiko-renderer-web/styles.css", from);
20
+ const stylesheet = Bun.resolveSync("@cosmicdrift/kumiko-renderer/web/styles.css", from);
21
21
  return join(dirname(stylesheet), "fonts");
22
22
  }
23
23
  catch {
@@ -1,6 +1,6 @@
1
1
  import { type ClientIpResolver } from "@cosmicdrift/kumiko-framework/api";
2
2
  import { type SessionUser } from "@cosmicdrift/kumiko-framework/engine";
3
- import type { PageHeadSystemQuery } from "@cosmicdrift/kumiko-headless/apex";
3
+ import type { PageHeadSystemQuery } from "@cosmicdrift/kumiko-renderer/headless/apex";
4
4
  export type QueryDispatcher = {
5
5
  readonly query: (type: string, payload: unknown, user: SessionUser) => Promise<unknown>;
6
6
  };
@@ -9,6 +9,6 @@ export declare function resolveTailwindCli(deps: ResolveTailwindCliDeps): string
9
9
  /** Tailwind v4 resolves `@import "tailwindcss"` relative to the entry
10
10
  * CSS file. Skip renderer-web fallback (or fail build) when peer deps
11
11
  * aren't resolvable from that directory — e.g. Bun cache copies of
12
- * `@cosmicdrift/kumiko-renderer-web` outside a hoisted node_modules tree. */
12
+ * `@cosmicdrift/kumiko-renderer/web` outside a hoisted node_modules tree. */
13
13
  export declare function canResolveTailwindStylesheet(entryCss: string, deps: ResolveTailwindCliDeps): boolean;
14
14
  export {};
@@ -22,7 +22,7 @@ export function resolveTailwindCli(deps) {
22
22
  /** Tailwind v4 resolves `@import "tailwindcss"` relative to the entry
23
23
  * CSS file. Skip renderer-web fallback (or fail build) when peer deps
24
24
  * aren't resolvable from that directory — e.g. Bun cache copies of
25
- * `@cosmicdrift/kumiko-renderer-web` outside a hoisted node_modules tree. */
25
+ * `@cosmicdrift/kumiko-renderer/web` outside a hoisted node_modules tree. */
26
26
  export function canResolveTailwindStylesheet(entryCss, deps) {
27
27
  if (deps.bun === undefined)
28
28
  return false;
@@ -14,6 +14,8 @@ export declare function addConfigAccessorFactory<T extends {
14
14
  readonly configResolver?: ConfigResolver;
15
15
  }>(resolved: T, registry: Registry): T;
16
16
  export declare function loggerFromExtraContext(extraContext: Record<string, unknown>): Logger | undefined;
17
+ /** Config key (`feature:config:name`) to value; wins over the key's env variable. */
18
+ export type ConfigOverridesInput = Readonly<Record<string, string | number | boolean>>;
17
19
  type BootExtraContextOptions = {
18
20
  readonly db: DbConnection;
19
21
  readonly features: readonly FeatureDefinition[];
@@ -33,6 +35,8 @@ type BootExtraContextOptions = {
33
35
  readonly log?: Logger;
34
36
  /** Signs `unsubscribe: true` notifications' mail links; see runProdApp `unsubscribe`. */
35
37
  readonly unsubscribeLinks?: UnsubscribeLinkConfig;
38
+ /** App-level config values (tenant model, branding, seo ...) above the env bridge. */
39
+ readonly appOverrides?: ConfigOverridesInput;
36
40
  };
37
41
  export declare function buildBootExtraContext(opts: BootExtraContextOptions): Record<string, unknown>;
38
42
  /** Die Auth-Felder die resolveAuthMail liest/normalisiert — beide
@@ -3,7 +3,7 @@ import { makeAuthPaths, } from "@cosmicdrift/kumiko-bundled-features/auth-email-
3
3
  import { resolveSessionStore } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
4
4
  import { bindMfaRevokeAllOtherSessionsFromFeature, bindRevokeAllPatTokensFromFeature, } from "@cosmicdrift/kumiko-bundled-features/auth-mfa";
5
5
  import { createSmtpTransportFromEnv } from "@cosmicdrift/kumiko-bundled-features/channel-email";
6
- import { buildEnvConfigOverrides, createConfigAccessorFactory, createConfigResolver, } from "@cosmicdrift/kumiko-bundled-features/config";
6
+ import { buildEnvConfigOverrides, createConfigAccessorFactory, createConfigResolver, validateAppOverrides, } from "@cosmicdrift/kumiko-bundled-features/config";
7
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";
@@ -23,9 +23,13 @@ import { resolveBootCrypto } from "./boot/boot-crypto.js";
23
23
  export function addConfigAccessorFactory(resolved, registry) {
24
24
  if (!resolved.configResolver)
25
25
  return resolved;
26
+ // Both keys, always overriding an app-supplied value: the factory must derive from the
27
+ // effective resolver (fw#3313); the underscore alias stays until the next major.
28
+ const configAccessorFactory = createConfigAccessorFactory(registry, resolved.configResolver);
26
29
  return {
27
30
  ...resolved,
28
- _configAccessorFactory: createConfigAccessorFactory(registry, resolved.configResolver),
31
+ configAccessorFactory,
32
+ _configAccessorFactory: configAccessorFactory,
29
33
  };
30
34
  }
31
35
  // Framework-Default-Provider für den AppContext — gleicher Mechanismus wie
@@ -117,9 +121,12 @@ function bootCryptoContext(crypto) {
117
121
  ...(crypto.configCipher && { configEncryption: crypto.configCipher }),
118
122
  };
119
123
  }
120
- function createBootConfigResolver(registry, envSource, crypto) {
124
+ function createBootConfigResolver(registry, envSource, crypto, appOverrides) {
125
+ const envOverrides = buildEnvConfigOverrides(registry, envSource);
121
126
  return createConfigResolver({
122
- appOverrides: buildEnvConfigOverrides(registry, envSource),
127
+ appOverrides: appOverrides
128
+ ? new Map([...envOverrides, ...validateAppOverrides(registry, appOverrides)])
129
+ : envOverrides,
123
130
  ...(crypto.configCipher && { cipher: crypto.configCipher }),
124
131
  });
125
132
  }
@@ -128,7 +135,7 @@ function createBootConfigResolver(registry, envSource, crypto) {
128
135
  // stays auth-only.
129
136
  function resolveDeliveryConfigResolver(opts, hasDeliveryFeature, crypto) {
130
137
  return opts.hasAuth || hasDeliveryFeature
131
- ? createBootConfigResolver(opts.registry, opts.envSource, crypto)
138
+ ? createBootConfigResolver(opts.registry, opts.envSource, crypto, opts.appOverrides)
132
139
  : undefined;
133
140
  }
134
141
  function bootDeliveryContext(opts, parts) {
@@ -1,7 +1,7 @@
1
1
  import { join, resolve, sep } from "node:path";
2
2
  import { cachedResponse, computeStrongEtag, computeWeakEtag, createClientIpResolver, } from "@cosmicdrift/kumiko-framework/api";
3
3
  import { rankPreferredLocales } from "@cosmicdrift/kumiko-framework/i18n";
4
- import { resolveAndInjectPageHead } from "@cosmicdrift/kumiko-headless/apex";
4
+ import { resolveAndInjectPageHead } from "@cosmicdrift/kumiko-renderer/headless/apex";
5
5
  import { errorCodeOf } from "@cosmicdrift/kumiko-types/type-guards";
6
6
  import { ASSETS_DIR } from "./build-prod-bundle.js";
7
7
  import { BUNDLED_ASSETS_DIST_DIR } from "./bundled-assets.js";
@@ -3,15 +3,14 @@ import { type AccountLockoutOptions, type AccountUnlockOptions, type AuthMailLoc
3
3
  import { type SeedAdminOptions } from "@cosmicdrift/kumiko-bundled-features/auth-email-password/seeding";
4
4
  import { type ExtraRouteDefinition, type KumikoServeEnv, type PostAuthLandingResolver, type SseBroker } from "@cosmicdrift/kumiko-framework/api";
5
5
  import { type KmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
6
- import { type EffectiveFeaturesResolver, type FeatureDefinition, type ValidateBootOptions } from "@cosmicdrift/kumiko-framework/engine";
6
+ import { type FeatureDefinition, type ValidateBootOptions } from "@cosmicdrift/kumiko-framework/engine";
7
7
  import { type AllInOneEntrypoint, type ApiEntrypoint } from "@cosmicdrift/kumiko-framework/entrypoint";
8
- import { type ComposedEnvSchema, KumikoBootError } from "@cosmicdrift/kumiko-framework/env";
9
- import { type DryRunEnvMode } from "@cosmicdrift/kumiko-framework/env/dry-run";
8
+ import type { ComposedEnvSchema, KumikoBootError } from "@cosmicdrift/kumiko-framework/env";
10
9
  import type { ErrorDocsConfig } from "@cosmicdrift/kumiko-framework/errors";
11
10
  import type { JobRunnerOptions } from "@cosmicdrift/kumiko-framework/jobs";
12
11
  import { type ObservabilityOptions, type ObservabilityProvider } from "@cosmicdrift/kumiko-framework/observability";
13
12
  import type { MasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
14
- import type { PageHeadMeta, PageHeadResolver, PageHeadSystemQuery } from "@cosmicdrift/kumiko-headless/apex";
13
+ import type { PageHeadMeta, PageHeadResolver, PageHeadSystemQuery } from "@cosmicdrift/kumiko-renderer/headless/apex";
15
14
  import { Redis } from "ioredis";
16
15
  import type { BillingBootOption } from "./boot-options/billing-option.ts";
17
16
  import type { DemoModeOption } from "./boot-options/demo-mode.ts";
@@ -23,15 +22,15 @@ import type { ProdSeedsConfig } from "./boot-options/seed-shortcuts.ts";
23
22
  import { type SystemWireDeps } from "./extra-routes-deps.ts";
24
23
  import { type RateLimitOption } from "./rate-limit-defaults.ts";
25
24
  import { type UnsubscribeOption } from "./resolve-unsubscribe-wiring.ts";
25
+ import { type ConfigOverridesInput } from "./run-prod-app-boot-context.ts";
26
26
  import { type SecurityHeadersOption } from "./security-headers.ts";
27
27
  import { type SeedCreateFileRef } from "./seed-dispatcher.ts";
28
+ export { parseRunMode } from "./boot/env-gate.ts";
28
29
  export { buildBunServeOptions } from "./bun-serve-options.ts";
29
30
  export { resolveUnsubscribeWiring, type UnsubscribeOption, } from "./resolve-unsubscribe-wiring.ts";
30
- export { addConfigAccessorFactory, buildBootExtraContext, loggerFromExtraContext, resolveAuthMail, } from "./run-prod-app-boot-context.ts";
31
+ export { addConfigAccessorFactory, buildBootExtraContext, type ConfigOverridesInput, loggerFromExtraContext, resolveAuthMail, } from "./run-prod-app-boot-context.ts";
31
32
  export { staticCachePolicy } from "./run-prod-app-static-files.ts";
32
33
  export declare function requireEnv(name: string, src?: Record<string, string | undefined>, context?: string): string;
33
- type RunMode = DryRunEnvMode;
34
- export declare function parseRunMode(raw: string | undefined): RunMode | null;
35
34
  /** Wrapper API for the password-reset flow.
36
35
  *
37
36
  * Since the delivery migration PasswordResetOptions carries `appUrl`
@@ -192,6 +191,9 @@ export type ProdSeedFn = (deps: {
192
191
  /** Stores bytes as a fileRef of `tenantId` (same validation and storage
193
192
  * layout as POST /files) without a helper write handler. */
194
193
  createFileRef: SeedCreateFileRef;
194
+ /** Present when a master key is configured: decrypts the same keys the handlers do, so a seed need not resolve the KEK itself. */
195
+ masterKeyProvider?: import("@cosmicdrift/kumiko-framework/secrets").MasterKeyProvider;
196
+ configEncryption?: import("@cosmicdrift/kumiko-framework/secrets").EnvelopeCipher;
195
197
  }) => Promise<void>;
196
198
  /** Boot-Time-Deps die `extraContext` + `anonymousAccess` Factories als
197
199
  * Argument bekommen. Closure dann in der returned Config (z.B. ein
@@ -206,6 +208,9 @@ export type RunProdAppDeps = {
206
208
  readonly sseBroker: SseBroker;
207
209
  /** The env the boot runs on: process env or `envSource`, with the boot-option defaults (e.g. `fileStorage`) underneath. */
208
210
  readonly env: Readonly<Record<string, string | undefined>>;
211
+ /** Present when a master key is configured; the same keyring the handlers use. */
212
+ readonly masterKeyProvider?: import("@cosmicdrift/kumiko-framework/secrets").MasterKeyProvider;
213
+ readonly configEncryption?: import("@cosmicdrift/kumiko-framework/secrets").EnvelopeCipher;
209
214
  };
210
215
  export type AnonymousAccessOption = import("@cosmicdrift/kumiko-framework/api").AnonymousAccessConfig | ((deps: RunProdAppDeps) => import("@cosmicdrift/kumiko-framework/api").AnonymousAccessConfig);
211
216
  export type ExtraContextOption = Record<string, unknown> | ((deps: RunProdAppDeps) => Record<string, unknown>);
@@ -394,6 +399,9 @@ export type RunProdAppOptions = {
394
399
  * @example
395
400
  * billing: { stripe: { priceToTier: { [env.STRIPE_PRICE_PRO]: "pro" } }, defaultTier: "free" } */
396
401
  readonly billing?: BillingBootOption;
402
+ /** Global operator switches: mounts `feature-toggles` and layers them over the tier resolver, so
403
+ * an operator can switch a feature off for every tenant. */
404
+ readonly featureToggles?: boolean;
397
405
  /** Live-demo mode: read-only gate in the fetch chain plus the anonymous
398
406
  * `GET /demo-mode` route. See `DemoModeOption`.
399
407
  * @example
@@ -418,6 +426,9 @@ export type RunProdAppOptions = {
418
426
  * jobs and queued delivery attempts (email/push/chat) stay pending and the
419
427
  * read side stays empty (2026-06-11 incident class). */
420
428
  readonly runSingleInstance?: boolean;
429
+ /** With `runSingleInstance: false`: the worker mounts the same mail / fileStorage features
430
+ * (its own boot options or by hand), so the boot warning about them stays silent. */
431
+ readonly workerMountsFeatures?: boolean;
421
432
  /** Job-Block. Wenn das Feature `r.job(...)` registriert, wird er
422
433
  * automatisch verdrahtet (siehe runSingleInstance). */
423
434
  readonly jobs?: {
@@ -474,11 +485,10 @@ export type RunProdAppOptions = {
474
485
  * the fetch-handler directly (Bun.serve isn't available under vitest +
475
486
  * node), then call handle.listen() manually if needed. */
476
487
  readonly autoListen?: boolean;
477
- /** Per-tenant feature resolver, passed to the dispatcher.
478
- * @deprecated Removed in the next release. `createTierEngineFeature({ tierMap })`
479
- * (or `composeSaasStack`) builds the resolver at boot, and prod-only features
480
- * come from the `mail`, `fileStorage` and `billing` options. */
481
- readonly effectiveFeatures?: EffectiveFeaturesResolver;
488
+ /** App-level config values (tenant model, branding, seo ...) keyed by the qualified config
489
+ * key. Validated against the registered keys at boot; they win over the key's env variable
490
+ * and sit below tenant and user values. */
491
+ readonly appOverrides?: ConfigOverridesInput;
482
492
  /** Composed Zod-schema for env-validation (from `composeEnvSchema({
483
493
  * core, features, extend })` in @cosmicdrift/kumiko-framework/env):
484
494
  * - `process.env` is parsed against it BEFORE any boot work; missing