@cosmicdrift/kumiko-server-runtime 0.388.0 → 0.389.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 (42) hide show
  1. package/dist/boot/shared-boot-steps.d.ts +6 -3
  2. package/dist/boot/shared-boot-steps.js +21 -15
  3. package/dist/boot/wait-for-database.d.ts +2 -0
  4. package/dist/boot/wait-for-database.js +4 -3
  5. package/dist/boot/worker-boot-core.js +9 -3
  6. package/dist/boot-options/demo-mode.d.ts +3 -3
  7. package/dist/boot-options/demo-mode.js +4 -1
  8. package/dist/boot-options/resolve-boot-options.d.ts +1 -0
  9. package/dist/boot-options/resolve-boot-options.js +2 -2
  10. package/dist/boot-options/stack-boot-requirements.d.ts +1 -1
  11. package/dist/boot-options/stack-boot-requirements.js +1 -13
  12. package/dist/build-prod-bundle.js +13 -13
  13. package/dist/bundled-lists-table.d.ts +2 -0
  14. package/dist/bundled-lists-table.js +56 -0
  15. package/dist/compose-auth-options.d.ts +16 -0
  16. package/dist/compose-auth-options.js +49 -0
  17. package/dist/compose-features.d.ts +5 -17
  18. package/dist/compose-features.js +6 -53
  19. package/dist/extra-routes-deps.d.ts +4 -0
  20. package/dist/extra-routes-deps.js +1 -0
  21. package/dist/files-for-tenant.d.ts +3 -0
  22. package/dist/files-for-tenant.js +10 -0
  23. package/dist/renderer-web-fonts.js +3 -9
  24. package/dist/resolve-installed-package.d.ts +10 -0
  25. package/dist/resolve-installed-package.js +32 -0
  26. package/dist/resolve-tailwind-cli.d.ts +3 -8
  27. package/dist/resolve-tailwind-cli.js +8 -25
  28. package/dist/run-prod-app-boot-context.d.ts +2 -0
  29. package/dist/run-prod-app-boot-context.js +8 -4
  30. package/dist/run-prod-app-config.js +1 -1
  31. package/dist/run-prod-app-connections.d.ts +2 -1
  32. package/dist/run-prod-app-connections.js +9 -5
  33. package/dist/run-prod-app-entrypoint.js +2 -0
  34. package/dist/run-prod-app-handle.js +2 -1
  35. package/dist/run-prod-app-seeds.js +2 -0
  36. package/dist/run-prod-app-static-files.js +9 -1
  37. package/dist/run-prod-app.d.ts +8 -2
  38. package/dist/run-prod-app.js +2 -2
  39. package/dist/run-worker-app.d.ts +11 -1
  40. package/dist/run-worker-app.js +2 -0
  41. package/package.json +19 -8
  42. package/src/changes.json +61 -0
@@ -2,16 +2,17 @@ import { type SeedAdminOptions } from "@cosmicdrift/kumiko-bundled-features/auth
2
2
  import type { AuthRoutesConfig } from "@cosmicdrift/kumiko-framework/api";
3
3
  import type { KmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
4
4
  import { type DbConnection } from "@cosmicdrift/kumiko-framework/db";
5
- import { type EffectiveFeaturesResolver, type FeatureDefinition, type Registry, type ValidateBootOptions } from "@cosmicdrift/kumiko-framework/engine";
5
+ import { type BundledListsOptions, type EffectiveFeaturesResolver, type FeatureDefinition, type Registry, type ValidateBootOptions } from "@cosmicdrift/kumiko-framework/engine";
6
6
  import type { CacheSyncBus } from "@cosmicdrift/kumiko-framework/redis";
7
7
  import type { EnvelopeCipher } from "@cosmicdrift/kumiko-framework/secrets";
8
8
  import type { FeatureTogglesWiring } from "../boot-options/feature-toggles-option.ts";
9
9
  import type { DbSeedFn } from "../boot-options/seed-shortcuts.ts";
10
10
  import { type AuthOptionsCarrier } from "../compose-features.ts";
11
11
  import type { RunProdAppAuthOptions } from "../run-prod-app.ts";
12
+ import { type DatabaseWaitClock } from "./wait-for-database.ts";
12
13
  /** Returns `envSource` itself (it may be `process.env`) when there is nothing to layer under it. */
13
14
  export declare function withEnvDefaults(envDefaults: Readonly<Record<string, string | undefined>>, envSource: Record<string, string | undefined>): Record<string, string | undefined>;
14
- export declare function composeAppFeatures(appFeatures: readonly FeatureDefinition[], bootFeatures: readonly FeatureDefinition[], effectiveAuth: AuthOptionsCarrier | undefined): readonly FeatureDefinition[];
15
+ export declare function composeAppFeatures(appFeatures: readonly FeatureDefinition[], bootFeatures: readonly FeatureDefinition[], effectiveAuth: AuthOptionsCarrier | undefined, bundledLists?: BundledListsOptions): readonly FeatureDefinition[];
15
16
  export declare function validateComposedFeatures(features: readonly FeatureDefinition[], bootOptions: ValidateBootOptions): void;
16
17
  export declare function assertMfaSwitchTenantGate(registry: Registry, processName: string): void;
17
18
  export declare function buildEffectiveFeaturesResolver(deps: {
@@ -27,13 +28,15 @@ export declare function applyStandardBootSeeds(deps: {
27
28
  readonly admin: SeedAdminOptions | undefined;
28
29
  readonly shortcutSeeds: readonly DbSeedFn[];
29
30
  readonly cipher: EnvelopeCipher | undefined;
31
+ readonly processName: string;
32
+ readonly databaseWait?: DatabaseWaitClock;
30
33
  }): Promise<void>;
31
34
  type SharedAuthFlowFields = Pick<RunProdAppAuthOptions, "passwordReset" | "emailVerification" | "accountUnlock" | "signup" | "invite">;
32
35
  type AuthFlowRoutes = Pick<AuthRoutesConfig, "passwordReset" | "emailVerification" | "accountUnlock" | "signup" | "invite">;
33
36
  export type SharedAuthRouteSource = SharedAuthFlowFields & Pick<RunProdAppAuthOptions, "loginErrorStatusMap" | "cookieDomain" | "retiredCookieDomains" | "allowedOrigins" | "unsafeSkipOriginCheck" | "postAuthLanding">;
34
37
  export type SharedAuthRoutes = AuthFlowRoutes & Pick<AuthRoutesConfig, "membershipQuery" | "loginHandler" | "loginErrorStatusMap" | "cookieDomain" | "retiredCookieDomains" | "allowedOrigins" | "unsafeSkipOriginCheck" | "postAuthLanding" | "mfaVerifyHandler" | "switchTenantMfaGateHandler" | "mfaPreauthEnableStartHandler" | "mfaPreauthConfirmHandler">;
35
38
  export declare function buildSharedAuthRoutes(effectiveAuth: SharedAuthRouteSource, hasMfa: boolean): SharedAuthRoutes;
36
- export declare function waitForKmsHealthy(kms: KmsAdapter, processName: string, deadline: number): Promise<void>;
39
+ export declare function waitForKmsHealthy(kms: KmsAdapter, processName: string, deadline: number, clock?: DatabaseWaitClock): Promise<void>;
37
40
  export declare function openDatabasePools(databaseUrl: string, envSource: Record<string, string | undefined>): {
38
41
  readonly db: DbConnection;
39
42
  readonly closeDb: () => Promise<void>;
@@ -15,11 +15,12 @@ import { waitUntilDatabaseReachable } from "./wait-for-database.js";
15
15
  export function withEnvDefaults(envDefaults, envSource) {
16
16
  return Object.keys(envDefaults).length > 0 ? { ...envDefaults, ...envSource } : envSource;
17
17
  }
18
- export function composeAppFeatures(appFeatures, bootFeatures, effectiveAuth) {
18
+ export function composeAppFeatures(appFeatures, bootFeatures, effectiveAuth, bundledLists) {
19
19
  const composeAuthOptions = buildComposeAuthOptions(effectiveAuth);
20
20
  return composeFeatures([...appFeatures, ...bootFeatures], {
21
21
  includeBundled: !!effectiveAuth,
22
22
  ...(composeAuthOptions && { authOptions: composeAuthOptions }),
23
+ ...(bundledLists && { bundledLists }),
23
24
  });
24
25
  }
25
26
  export function validateComposedFeatures(features, bootOptions) {
@@ -46,19 +47,23 @@ export async function buildEffectiveFeaturesResolver(deps) {
46
47
  return bound.resolver;
47
48
  }
48
49
  export async function applyStandardBootSeeds(deps) {
49
- const { db, registry, admin, shortcutSeeds, cipher } = deps;
50
- if (admin) {
51
- await seedAdminGuarded(db, admin);
52
- }
53
- for (const seed of shortcutSeeds) {
54
- await seed(db);
55
- }
56
- // The helper indirection is what config-seed-boot.integration.ts pins — keep it as a single call.
57
- await applyBootSeeds({
58
- registry,
59
- db,
60
- ...(cipher && { cipher }),
61
- });
50
+ const { db, registry, admin, shortcutSeeds, cipher, processName, databaseWait } = deps;
51
+ // Seeds are idempotent, so a transient connection error re-runs the whole sequence; any other
52
+ // error stays fatal. Later boot steps build on the seeds, so this cannot continue in the background.
53
+ await waitUntilDatabaseReachable(async () => {
54
+ if (admin) {
55
+ await seedAdminGuarded(db, admin);
56
+ }
57
+ for (const seed of shortcutSeeds) {
58
+ await seed(db);
59
+ }
60
+ // The helper indirection is what config-seed-boot.integration.ts pins — keep it as a single call.
61
+ await applyBootSeeds({
62
+ registry,
63
+ db,
64
+ ...(cipher && { cipher }),
65
+ });
66
+ }, { ...databaseWait, processName, target: "database (boot seeds)" });
62
67
  }
63
68
  const MFA_ROUTE_HANDLERS = {
64
69
  mfaVerifyHandler: AuthMfaHandlers.verify,
@@ -129,8 +134,9 @@ export function buildSharedAuthRoutes(effectiveAuth, hasMfa) {
129
134
  ...buildAuthFlowRoutes(effectiveAuth),
130
135
  };
131
136
  }
132
- export async function waitForKmsHealthy(kms, processName, deadline) {
137
+ export async function waitForKmsHealthy(kms, processName, deadline, clock) {
133
138
  const kmsHealth = await waitUntilDatabaseReachable(() => kms.health(), {
139
+ ...clock,
134
140
  processName,
135
141
  target: "KMS",
136
142
  deadline,
@@ -12,5 +12,7 @@ export type WaitForDatabaseOptions = {
12
12
  readonly now?: () => number;
13
13
  readonly deadline?: number;
14
14
  };
15
+ /** Injectable clock and sink; tests drive the backoff without real waiting. */
16
+ export type DatabaseWaitClock = Pick<WaitForDatabaseOptions, "log" | "sleep" | "now">;
15
17
  export declare function waitForDatabase(db: DbConnection, options: WaitForDatabaseOptions): Promise<void>;
16
18
  export declare function waitUntilDatabaseReachable<T>(ping: () => Promise<T>, options: WaitForDatabaseOptions): Promise<T>;
@@ -7,9 +7,10 @@ const TRANSIENT_CODES = new Set([
7
7
  "ETIMEDOUT",
8
8
  "ENOTFOUND",
9
9
  "EAI_AGAIN",
10
- "CONNECTION_CLOSED",
11
- "CONNECT_TIMEOUT",
12
- "CONNECTION_DESTROYED",
10
+ "ERR_POSTGRES_CONNECTION_CLOSED",
11
+ "ERR_POSTGRES_CONNECTION_REFUSED",
12
+ "ERR_POSTGRES_CONNECTION_TIMEOUT",
13
+ "ERR_POSTGRES_CONNECTION_FAILED",
13
14
  ]);
14
15
  const CONNECTION_EXCEPTION_CLASS = "08";
15
16
  export function isTransientConnectionError(error) {
@@ -67,6 +67,7 @@ export async function bootWorkerProcess(options, envSource, profile) {
67
67
  const features = composeFeatures(options.features, {
68
68
  includeBundled,
69
69
  ...(profile.authOptions && { authOptions: profile.authOptions }),
70
+ ...(options.bundledLists && { bundledLists: options.bundledLists }),
70
71
  });
71
72
  const bootCrypto = resolveBootCrypto(envSource, options.masterKey);
72
73
  validateBoot(features, {
@@ -92,9 +93,9 @@ export async function bootWorkerProcess(options, envSource, profile) {
92
93
  return { kind: "dry-run", envSource };
93
94
  }
94
95
  // Before the connections, so an abort leaks nothing.
95
- const databaseWaitDeadline = Date.now() + WAIT_FOR_DATABASE_DEADLINE_MS;
96
+ const databaseWaitDeadline = (options.databaseWait?.now ?? Date.now)() + WAIT_FOR_DATABASE_DEADLINE_MS;
96
97
  if (options.kms) {
97
- await waitForKmsHealthy(options.kms, processName, databaseWaitDeadline);
98
+ await waitForKmsHealthy(options.kms, processName, databaseWaitDeadline, options.databaseWait);
98
99
  }
99
100
  const { db, closeDb, outsideTransactionDb, closeOutsideTransactionDb } = openDatabasePools(databaseUrl, envSource);
100
101
  const redis = new Redis(redisUrl, { maxRetriesPerRequest: null });
@@ -103,7 +104,11 @@ export async function bootWorkerProcess(options, envSource, profile) {
103
104
  clientOptions: redisClientOptionsFromEnv(envSource),
104
105
  });
105
106
  try {
106
- await waitForDatabase(db, { processName, deadline: databaseWaitDeadline });
107
+ await waitForDatabase(db, {
108
+ ...options.databaseWait,
109
+ processName,
110
+ deadline: databaseWaitDeadline,
111
+ });
107
112
  }
108
113
  catch (err) {
109
114
  await closeDb();
@@ -151,6 +156,7 @@ export async function bootWorkerProcess(options, envSource, profile) {
151
156
  registry,
152
157
  hasAuth: includeBundled,
153
158
  redis,
159
+ ...(options.appOverrides && { appOverrides: options.appOverrides }),
154
160
  crypto: bootCrypto,
155
161
  ...(options.kms && { kms: options.kms }),
156
162
  ...(profile.deliverQueuedInline === true && { deliverQueuedInline: true }),
@@ -1,4 +1,4 @@
1
- import type { ExtraRouteDefinition } from "@cosmicdrift/kumiko-framework/api";
1
+ import { type ExtraRouteDefinition } from "@cosmicdrift/kumiko-framework/api";
2
2
  export type DemoAccountRole = "admin" | "sysadmin";
3
3
  /** A role alone, or a role with the label the banner shows next to the login. */
4
4
  export type DemoAccountEntry = DemoAccountRole | {
@@ -19,7 +19,7 @@ export type DemoModeNotice = {
19
19
  readonly accounts?: readonly DemoAccountEntry[];
20
20
  };
21
21
  /** Live-demo switch. `readOnly: true` answers every mutating request
22
- * (POST/PUT/PATCH/DELETE) outside `/api/query` and the login/logout/MFA
22
+ * (POST/PUT/PATCH/DELETE) and every WebSocket upgrade outside `/api/query` and the login/logout/MFA
23
23
  * routes with 403 `demo_read_only`. `GET /demo-mode` (anonymous) returns
24
24
  * `{ readOnly, notice }` for the client banner.
25
25
  *
@@ -32,7 +32,7 @@ export type DemoModeNotice = {
32
32
  export type DemoModeOption = {
33
33
  readonly readOnly: boolean;
34
34
  readonly notice?: DemoModeNotice;
35
- /** Mutating paths that stay open besides the default allowlist. */
35
+ /** Mutating paths (and WebSocket route paths) that stay open besides the default allowlist. */
36
36
  readonly allowedWritePaths?: readonly string[];
37
37
  };
38
38
  export declare const DEMO_MODE_PATH = "/demo-mode";
@@ -1,3 +1,4 @@
1
+ import { isWebSocketUpgradeRequest, } from "@cosmicdrift/kumiko-framework/api";
1
2
  export const DEMO_MODE_PATH = "/demo-mode";
2
3
  export const DEMO_READ_ONLY_ERROR_CODE = "demo_read_only";
3
4
  const MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
@@ -52,7 +53,9 @@ export function withDemoReadOnlyFetch(fetchHandler, demoMode) {
52
53
  ...(demoMode.allowedWritePaths ?? []),
53
54
  ]);
54
55
  return async (req, arg) => {
55
- if (MUTATING_METHODS.has(req.method) && !allowed.has(new URL(req.url).pathname)) {
56
+ // A socket carries writes after the upgrade (route `write` dep), outside this method gate.
57
+ const mutates = MUTATING_METHODS.has(req.method) || isWebSocketUpgradeRequest(req);
58
+ if (mutates && !allowed.has(new URL(req.url).pathname)) {
56
59
  return Response.json({
57
60
  isSuccess: false,
58
61
  error: {
@@ -24,6 +24,7 @@ export type BootOptionFields = {
24
24
  /** With `runSingleInstance: false`: the worker process mounts the same mail / fileStorage
25
25
  * features (its own boot options or by hand), which silences the boot warning. */
26
26
  readonly workerMountsFeatures?: boolean;
27
+ readonly appOverrides?: object;
27
28
  readonly auth?: {
28
29
  readonly admin?: unknown;
29
30
  readonly mail?: {
@@ -46,9 +46,9 @@ function wireFeatureToggles(options, mounted) {
46
46
  function warnAboutBootOptions(options) {
47
47
  const bootWarnings = stackBootWarnings(options).map((line) => `stacks in \`features\` expect boot options that are not set:\n${line}`);
48
48
  if (options.runSingleInstance === false &&
49
- (options.mail || options.fileStorage || options.featureToggles) &&
49
+ (options.mail || options.fileStorage || options.featureToggles || options.appOverrides) &&
50
50
  options.workerMountsFeatures !== true) {
51
- 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
+ bootWarnings.push("runSingleInstance:false — delivery and file jobs run in runWorkerApp. Pass the same mail / fileStorage / featureToggles / appOverrides options there, or set workerMountsFeatures: true if it mounts those features itself.");
52
52
  }
53
53
  if (bootWarnings.length > 0) {
54
54
  // biome-ignore lint/suspicious/noConsole: boot-time warn, no logger configured this early
@@ -3,7 +3,7 @@ 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
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`). */
6
+ * options the calling boot function can set at all. */
7
7
  export declare function assertStackBootRequirements(options: StackBootOptionFields, scope?: readonly BootOptionName[]): void;
8
8
  /** Requirements a stack marked `severity: "warn"`: logged at boot, never fatal. */
9
9
  export declare function stackBootWarnings(options: StackBootOptionFields): string[];
@@ -29,18 +29,6 @@ const OPTION_CHECKS = {
29
29
  isSatisfied: (options) => options.featureToggles === true,
30
30
  hint: "set featureToggles: true",
31
31
  },
32
- demoMode: {
33
- isSatisfied: (options) => options.demoMode !== undefined,
34
- hint: "set demoMode: { readOnly }",
35
- },
36
- seeds: {
37
- isSatisfied: (options) => options.seeds !== undefined,
38
- hint: "set seeds",
39
- },
40
- signals: {
41
- isSatisfied: (options) => options.signals !== false,
42
- hint: "drop signals: false",
43
- },
44
32
  rateLimit: {
45
33
  isSatisfied: (options) => options.rateLimit !== false,
46
34
  hint: "drop rateLimit: false",
@@ -65,7 +53,7 @@ function unsatisfiedRequirements(options, severity, scope) {
65
53
  }
66
54
  /** Reads `bootRequires` from the raw feature list: dedupe may later drop the
67
55
  * 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`). */
56
+ * options the calling boot function can set at all. */
69
57
  export function assertStackBootRequirements(options, scope) {
70
58
  const gaps = unsatisfiedRequirements(options, "error", scope);
71
59
  if (gaps.length > 0) {
@@ -63,6 +63,7 @@ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
63
63
  import { BUNDLED_ASSETS_DIST_DIR, readBundledAssetDeclarations, resolveBundledAssetSource, } from "./bundled-assets.js";
64
64
  import { CSP_HASHES_FILE, inlineScriptHashes } from "./csp-hashes.js";
65
65
  import { RENDERER_WEB_FONT_FILE_PATTERN, RENDERER_WEB_FONTS_DIST_DIR, resolveRendererWebFontsDir, } from "./renderer-web-fonts.js";
66
+ import { resolveInstalledSync } from "./resolve-installed-package.js";
66
67
  import { canResolveTailwindStylesheet, resolveTailwindCli } from "./resolve-tailwind-cli.js";
67
68
  // Bun runtime check as a module-level constant: every build step (Tailwind via
68
69
  // Bun.spawn, client bundling via Bun.build, and stylesheet resolution via
@@ -454,18 +455,11 @@ function resolveStylesheetEntry(cwd, clientEntry, override) {
454
455
  // and production builds behave identically.
455
456
  if (!clientEntry)
456
457
  return undefined;
457
- if (!hasBun)
458
- return undefined;
459
- try {
460
- const resolved = Bun.resolveSync("@cosmicdrift/kumiko-renderer/web/styles.css", cwd);
461
- if (!canResolveTailwindStylesheet(resolved, { bun: Bun, cwd })) {
462
- return undefined;
463
- }
464
- return { path: resolved, isRendererWebFallback: true };
465
- }
466
- catch {
458
+ const resolved = resolveInstalledSync("@cosmicdrift/kumiko-renderer/web/styles.css", cwd);
459
+ if (resolved === undefined || !canResolveTailwindStylesheet(resolved, { bun: Bun, cwd })) {
467
460
  return undefined;
468
461
  }
462
+ return { path: resolved, isRendererWebFallback: true };
469
463
  }
470
464
  // Build-time guard against unstyled production output: when the build falls
471
465
  // back to renderer-web/styles.css but the compiled CSS lacks the shell sentinel,
@@ -579,9 +573,15 @@ function isTailwindEngine(candidate) {
579
573
  // Both packages are the CLI's own dependencies, so they resolve from the CLI package directory
580
574
  // (same version the CLI would run), not from the app root.
581
575
  async function loadTailwindEngine(cliPath, bun) {
582
- const cliDir = dirname(bun.resolveSync("@tailwindcss/cli/package.json", dirname(cliPath)));
583
- const node = await import(__rewriteRelativeImportExtension(bun.resolveSync("@tailwindcss/node", cliDir)));
584
- const oxide = await import(__rewriteRelativeImportExtension(bun.resolveSync("@tailwindcss/oxide", cliDir)));
576
+ const cliDir = resolve(cliPath, "..", "..");
577
+ const importFromCli = (packageName) => {
578
+ const entry = resolveInstalledSync(packageName, cliDir, bun);
579
+ if (entry !== undefined)
580
+ return import(__rewriteRelativeImportExtension(entry));
581
+ throw new Error(`[kumiko build] ${packageName} is not installed next to @tailwindcss/cli (${cliDir}).`);
582
+ };
583
+ const node = await importFromCli("@tailwindcss/node");
584
+ const oxide = await importFromCli("@tailwindcss/oxide");
585
585
  const engine = {
586
586
  compile: isRecord(node) ? node["compile"] : undefined,
587
587
  optimize: isRecord(node) ? node["optimize"] : undefined,
@@ -0,0 +1,2 @@
1
+ import { type BundledListsOptions, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
2
+ export declare function applyBundledListsTable(features: readonly FeatureDefinition[], options: BundledListsOptions | undefined): FeatureDefinition[];
@@ -0,0 +1,56 @@
1
+ import { normalizeListColumn, validateBundledListsOptions, } from "@cosmicdrift/kumiko-framework/engine";
2
+ export function applyBundledListsTable(features, options) {
3
+ if (options === undefined)
4
+ return [...features];
5
+ validateBundledListsOptions(options);
6
+ return features.map((feature) => feature.adoptsBundledLists === true ? withListTableDefaults(feature, options) : feature);
7
+ }
8
+ function withListTableDefaults(feature, options) {
9
+ return {
10
+ ...feature,
11
+ screens: Object.fromEntries(Object.entries(feature.screens).map(([id, screen]) => [
12
+ id,
13
+ withScreenTableDefaults(screen, options),
14
+ ])),
15
+ };
16
+ }
17
+ function withScreenTableDefaults(screen, options) {
18
+ switch (screen.type) {
19
+ case "entityList":
20
+ case "projectionList": {
21
+ const table = { ...options.table, ...screen.table };
22
+ return { ...screen, table, columns: pinFirstColumn(screen.columns, table, options) };
23
+ }
24
+ case "projectionDetail":
25
+ case "entityEdit":
26
+ return {
27
+ ...screen,
28
+ layout: {
29
+ ...screen.layout,
30
+ sections: screen.layout.sections.map((section) => withSectionTableDefaults(section, options)),
31
+ },
32
+ };
33
+ default:
34
+ return screen;
35
+ }
36
+ }
37
+ function withSectionTableDefaults(section, options) {
38
+ if (section.kind !== "relatedList")
39
+ return section;
40
+ // export is only valid on entityList and projectionList
41
+ const { export: _export, ...defaultsWithoutExport } = options.table;
42
+ const table = { ...defaultsWithoutExport, ...section.table };
43
+ return { ...section, table, columns: pinFirstColumn(section.columns, table, options) };
44
+ }
45
+ function pinFirstColumn(columns, table, options) {
46
+ if (options.pinFirstColumn !== true || table.pinning !== true)
47
+ return columns;
48
+ const normalized = columns.map(normalizeListColumn);
49
+ if (normalized.some((column) => column.pinned !== undefined))
50
+ return columns;
51
+ // A hidden column only appears via the column picker; pinning it would surprise the user.
52
+ const firstVisible = normalized.findIndex((column) => column.hidden !== true);
53
+ if (firstVisible === -1)
54
+ return columns;
55
+ return columns.map((column, index) => index === firstVisible ? { ...normalizeListColumn(column), pinned: "left" } : column);
56
+ }
@@ -0,0 +1,16 @@
1
+ import type { AccountLockoutOptions, AccountUnlockOptions, AuthEmailPasswordOptions, EmailVerificationOptions, InviteOptions, PasswordResetOptions, SignupOptions } from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
2
+ /** Shape of any run{Prod,Dev}App auth block that can carry a
3
+ * passwordReset/emailVerification config, once resolveAuthMail has resolved
4
+ * the wrapper's optional hmacSecret into the feature API's required one —
5
+ * a structural-typed lookup on the auth-only subset is enough. Lets
6
+ * buildComposeAuthOptions be called with both RunProd- and
7
+ * RunDev-AuthOptions without building the helper twice. */
8
+ export type AuthOptionsCarrier = {
9
+ readonly passwordReset?: PasswordResetOptions;
10
+ readonly emailVerification?: EmailVerificationOptions;
11
+ readonly signup?: SignupOptions;
12
+ readonly invite?: InviteOptions;
13
+ readonly accountUnlock?: AccountUnlockOptions;
14
+ readonly accountLockout?: AccountLockoutOptions;
15
+ };
16
+ export declare function buildComposeAuthOptions(auth: AuthOptionsCarrier | undefined): AuthEmailPasswordOptions | undefined;
@@ -0,0 +1,49 @@
1
+ // The magic-link mail fields shared by all five flows. Conditional spreads omit
2
+ // undefined keys (exactOptionalPropertyTypes) and avoid the property-write that
3
+ // trips noPropertyAccessFromIndexSignature on the type-aliased option shapes.
4
+ // reset/verify layer hmacSecret (+ mode) on top.
5
+ function pickMailFields(src) {
6
+ return {
7
+ appUrl: src.appUrl,
8
+ ...(src.tokenTtlMinutes !== undefined && { tokenTtlMinutes: src.tokenTtlMinutes }),
9
+ ...(src.appName !== undefined && { appName: src.appName }),
10
+ ...(src.locale !== undefined && { locale: src.locale }),
11
+ };
12
+ }
13
+ export function buildComposeAuthOptions(auth) {
14
+ if (!auth)
15
+ return undefined;
16
+ const opts = {};
17
+ if (auth.passwordReset) {
18
+ opts.passwordReset = {
19
+ hmacSecret: auth.passwordReset.hmacSecret,
20
+ ...pickMailFields(auth.passwordReset),
21
+ };
22
+ }
23
+ if (auth.emailVerification) {
24
+ opts.emailVerification = {
25
+ hmacSecret: auth.emailVerification.hmacSecret,
26
+ ...pickMailFields(auth.emailVerification),
27
+ ...(auth.emailVerification.mode !== undefined && { mode: auth.emailVerification.mode }),
28
+ };
29
+ }
30
+ if (auth.signup) {
31
+ opts.signup = pickMailFields(auth.signup);
32
+ }
33
+ if (auth.invite) {
34
+ opts.invite = pickMailFields(auth.invite);
35
+ }
36
+ if (auth.accountUnlock) {
37
+ opts.accountUnlock = {
38
+ hmacSecret: auth.accountUnlock.hmacSecret,
39
+ ...pickMailFields(auth.accountUnlock),
40
+ };
41
+ }
42
+ if (auth.accountLockout) {
43
+ opts.accountLockout = auth.accountLockout;
44
+ }
45
+ return hasAnyAuthFlow(opts) ? opts : undefined;
46
+ }
47
+ function hasAnyAuthFlow(opts) {
48
+ return Object.values(opts).some(Boolean);
49
+ }
@@ -1,5 +1,6 @@
1
- import { type AccountLockoutOptions, type AccountUnlockOptions, type AuthEmailPasswordOptions, type EmailVerificationOptions, type InviteOptions, type PasswordResetOptions, type SignupOptions } from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
2
- import { type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
1
+ import { type AuthEmailPasswordOptions } from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
2
+ import { type BundledListsOptions, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
3
+ export { type AuthOptionsCarrier, buildComposeAuthOptions } from "./compose-auth-options.ts";
3
4
  export type ComposeFeaturesOptions = {
4
5
  /** When true, prepends config + user + tenant + auth-email-password
5
6
  * (+ auth-self-registration when authOptions.signup is set) before the
@@ -12,20 +13,7 @@ export type ComposeFeaturesOptions = {
12
13
  * auth-routes.ts but no handler dispatches). Goes hand-in-hand with the
13
14
  * passwordReset block in RunProdAppAuthOptions / RunDevAppAuthOptions. */
14
15
  readonly authOptions?: AuthEmailPasswordOptions;
16
+ /** One table block for every list screen and relatedList of the bundled features; screen-own keys win. */
17
+ readonly bundledLists?: BundledListsOptions;
15
18
  };
16
19
  export declare function composeFeatures(appFeatures: readonly FeatureDefinition[], options: ComposeFeaturesOptions): FeatureDefinition[];
17
- /** Shape of any run{Prod,Dev}App auth block that can carry a
18
- * passwordReset/emailVerification config, once resolveAuthMail has resolved
19
- * the wrapper's optional hmacSecret into the feature API's required one —
20
- * a structural-typed lookup on the auth-only subset is enough. Lets
21
- * buildComposeAuthOptions be called with both RunProd- and
22
- * RunDev-AuthOptions without building the helper twice. */
23
- export type AuthOptionsCarrier = {
24
- readonly passwordReset?: PasswordResetOptions;
25
- readonly emailVerification?: EmailVerificationOptions;
26
- readonly signup?: SignupOptions;
27
- readonly invite?: InviteOptions;
28
- readonly accountUnlock?: AccountUnlockOptions;
29
- readonly accountLockout?: AccountLockoutOptions;
30
- };
31
- export declare function buildComposeAuthOptions(auth: AuthOptionsCarrier | undefined): AuthEmailPasswordOptions | undefined;
@@ -21,9 +21,11 @@ import { createTemplateResolverFeature } from "@cosmicdrift/kumiko-bundled-featu
21
21
  import { collectAssignableAppRoles, createTenantFeature, } from "@cosmicdrift/kumiko-bundled-features/tenant";
22
22
  import { TENANT_LIFECYCLE_FEATURE } from "@cosmicdrift/kumiko-bundled-features/tenant-lifecycle";
23
23
  import { createUserFeature } from "@cosmicdrift/kumiko-bundled-features/user";
24
- import { dedupeFeatures } from "@cosmicdrift/kumiko-framework/engine";
24
+ import { dedupeFeatures, } from "@cosmicdrift/kumiko-framework/engine";
25
25
  import { FILES_FEATURE } from "@cosmicdrift/kumiko-framework/files";
26
26
  import { createRendererMailHtmlFeature } from "@cosmicdrift/kumiko-renderer-mail-html";
27
+ import { applyBundledListsTable } from "./bundled-lists-table.js";
28
+ export { buildComposeAuthOptions } from "./compose-auth-options.js";
27
29
  export function composeFeatures(appFeatures, options) {
28
30
  // ponytail: includeBundled:false skips the auth-mfa auto-wiring below —
29
31
  // an app composing its own foundation and mounting auth-mfa itself is
@@ -32,7 +34,7 @@ export function composeFeatures(appFeatures, options) {
32
34
  // bypasses MFA. Upgrade if this trips someone: warn here when appFeatures
33
35
  // contains AUTH_MFA_FEATURE but includeBundled is false.
34
36
  if (!options.includeBundled)
35
- return dedupeFeatures(appFeatures);
37
+ return applyBundledListsTable(dedupeFeatures(appFeatures), options.bundledLists);
36
38
  // Bundled foundation goes first so its instances carry the runDevApp /
37
39
  // runProdApp `authOptions` (passwordReset wiring etc.). App-features that
38
40
  // ALSO declare one of these names — e.g. the create-kumiko-app picker
@@ -92,12 +94,12 @@ export function composeFeatures(appFeatures, options) {
92
94
  const needsFilesTenantData = names.has(FILES_FEATURE) &&
93
95
  names.has(TENANT_LIFECYCLE_FEATURE) &&
94
96
  !names.has(FILES_TENANT_DATA_FEATURE);
95
- return [
97
+ return applyBundledListsTable([
96
98
  ...bundled,
97
99
  ...missingAuthMailStack(names),
98
100
  ...filteredApp,
99
101
  ...(needsFilesTenantData ? [createFilesTenantDataFeature()] : []),
100
- ];
102
+ ], options.bundledLists);
101
103
  }
102
104
  // auth-email-password declares its mails as stored templates, and the boot seed
103
105
  // fails without the template-resolver and a mail renderer. Mounting the missing
@@ -110,52 +112,3 @@ function missingAuthMailStack(mountedNames) {
110
112
  createRendererMailHtmlFeature(),
111
113
  ].filter((feature) => !mountedNames.has(feature.name));
112
114
  }
113
- // The magic-link mail fields shared by all five flows. Conditional spreads omit
114
- // undefined keys (exactOptionalPropertyTypes) and avoid the property-write that
115
- // trips noPropertyAccessFromIndexSignature on the type-aliased option shapes.
116
- // reset/verify layer hmacSecret (+ mode) on top.
117
- function pickMailFields(src) {
118
- return {
119
- appUrl: src.appUrl,
120
- ...(src.tokenTtlMinutes !== undefined && { tokenTtlMinutes: src.tokenTtlMinutes }),
121
- ...(src.appName !== undefined && { appName: src.appName }),
122
- ...(src.locale !== undefined && { locale: src.locale }),
123
- };
124
- }
125
- export function buildComposeAuthOptions(auth) {
126
- if (!auth)
127
- return undefined;
128
- const opts = {};
129
- if (auth.passwordReset) {
130
- opts.passwordReset = {
131
- hmacSecret: auth.passwordReset.hmacSecret,
132
- ...pickMailFields(auth.passwordReset),
133
- };
134
- }
135
- if (auth.emailVerification) {
136
- opts.emailVerification = {
137
- hmacSecret: auth.emailVerification.hmacSecret,
138
- ...pickMailFields(auth.emailVerification),
139
- ...(auth.emailVerification.mode !== undefined && { mode: auth.emailVerification.mode }),
140
- };
141
- }
142
- if (auth.signup) {
143
- opts.signup = pickMailFields(auth.signup);
144
- }
145
- if (auth.invite) {
146
- opts.invite = pickMailFields(auth.invite);
147
- }
148
- if (auth.accountUnlock) {
149
- opts.accountUnlock = {
150
- hmacSecret: auth.accountUnlock.hmacSecret,
151
- ...pickMailFields(auth.accountUnlock),
152
- };
153
- }
154
- if (auth.accountLockout) {
155
- opts.accountLockout = auth.accountLockout;
156
- }
157
- return hasAnyAuthFlow(opts) ? opts : undefined;
158
- }
159
- function hasAnyAuthFlow(opts) {
160
- return Object.values(opts).some(Boolean);
161
- }
@@ -1,11 +1,13 @@
1
1
  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
+ import type { FileContext } from "@cosmicdrift/kumiko-framework/files";
4
5
  import type { MetricsHandle } from "@cosmicdrift/kumiko-framework/observability";
5
6
  import type { EnvelopeCipher, MasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
6
7
  import type { Redis } from "ioredis";
7
8
  export type { SystemDispatchArgs } from "@cosmicdrift/kumiko-framework/api";
8
9
  export { makeDispatchSystemQuery, makeDispatchSystemWrite, } from "@cosmicdrift/kumiko-framework/api";
10
+ export { makeFilesForTenant } from "./files-for-tenant.ts";
9
11
  /** Deps for the `wire` hook (runProdApp/createKumikoServer, after
10
12
  * buildServer has mounted `extraRoutes`) and
11
13
  * for `runWorkerApp.wireComponents`. Naming: `deps` not `ctx` because
@@ -18,6 +20,8 @@ export type SystemWireDeps = {
18
20
  /** Feature-registry — e.g. for plugin-lookups via
19
21
  * `registry.getExtensionUsages("subscriptionProvider")`. */
20
22
  readonly registry: Registry;
23
+ /** File context of a tenant's storage; undefined when no file storage is mounted. Required so a consumer cannot lose attachment reading by omission. */
24
+ readonly filesForTenant: (tenantId: string) => FileContext | undefined;
21
25
  /** The boot's master key keyring; absent when no `KUMIKO_SECRETS_MASTER_KEY_V<n>` is set. */
22
26
  readonly masterKeyProvider?: MasterKeyProvider;
23
27
  /** Cipher for `encrypted: true` config keys, same instance and DEK cache as the handlers use. */
@@ -2,3 +2,4 @@
2
2
  // truth lives in framework/api next to buildServer's own `extraRoutes`
3
3
  // mount so the two never drift on how the SystemAdmin identity is built.
4
4
  export { makeDispatchSystemQuery, makeDispatchSystemWrite, } from "@cosmicdrift/kumiko-framework/api";
5
+ export { makeFilesForTenant } from "./files-for-tenant.js";
@@ -0,0 +1,3 @@
1
+ import { type AppContext } from "@cosmicdrift/kumiko-framework/engine";
2
+ import type { FileContext } from "@cosmicdrift/kumiko-framework/files";
3
+ export declare function makeFilesForTenant(context: AppContext): (tenantId: string) => FileContext | undefined;
@@ -0,0 +1,10 @@
1
+ import { fileContextFromResolver } from "@cosmicdrift/kumiko-bundled-features/file-foundation";
2
+ import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
3
+ // Takes the AppContext buildServer returned: only that one carries `_fileProviderResolver`.
4
+ // Without a resolver, `context.files` is bound to the system tenant and reaches no other one.
5
+ export function makeFilesForTenant(context) {
6
+ const resolver = context._fileProviderResolver;
7
+ if (resolver)
8
+ return (tenantId) => fileContextFromResolver(resolver, tenantId);
9
+ return (tenantId) => (tenantId === SYSTEM_TENANT_ID ? context.files : undefined);
10
+ }
@@ -3,26 +3,20 @@
3
3
  // dir (dev) or hashed into dist/assets (prod), away from the source fonts.
4
4
  // data: URIs are out because apps run with `font-src 'self'`.
5
5
  import { dirname, join } from "node:path";
6
+ import { resolveInstalledSync } from "./resolve-installed-package.js";
6
7
  export const RENDERER_WEB_FONTS_URL_PREFIX = "/assets/kumiko/fonts/";
7
8
  /** dist-relative folder the prod build copies the fonts into. */
8
9
  export const RENDERER_WEB_FONTS_DIST_DIR = "assets/kumiko/fonts";
9
10
  /** Allowlist for servable font file names; also rules out path segments. */
10
11
  export const RENDERER_WEB_FONT_FILE_PATTERN = /^[a-z0-9-]+\.woff2$/;
11
- const hasBun = typeof Bun !== "undefined";
12
12
  /** Directory of the woff2 files next to renderer-web's styles.css, or
13
13
  * undefined when the package can't be resolved from any `searchFrom` dir
14
14
  * (or outside Bun, which does the package-exports resolution). */
15
15
  export function resolveRendererWebFontsDir(searchFrom) {
16
- if (!hasBun)
17
- return undefined;
18
16
  for (const from of searchFrom) {
19
- try {
20
- const stylesheet = Bun.resolveSync("@cosmicdrift/kumiko-renderer/web/styles.css", from);
17
+ const stylesheet = resolveInstalledSync("@cosmicdrift/kumiko-renderer/web/styles.css", from);
18
+ if (stylesheet !== undefined)
21
19
  return join(dirname(stylesheet), "fonts");
22
- }
23
- catch {
24
- // try the next search root
25
- }
26
20
  }
27
21
  return undefined;
28
22
  }
@@ -0,0 +1,10 @@
1
+ export type BunResolver = {
2
+ readonly resolveSync: (id: string, from: string) => string;
3
+ };
4
+ export declare function packageNameOf(specifier: string): string;
5
+ /** Walks node_modules upwards like Node does; returns the real path so transitive deps
6
+ * of an isolated-linker install are found as siblings. */
7
+ export declare function findInstalledPackageDir(packageName: string, fromDir: string): string | undefined;
8
+ /** Bun.resolveSync auto-installs from the registry (since 1.4.4) when no node_modules tree
9
+ * exists, so only call it for packages that are already installed. */
10
+ export declare function resolveInstalledSync(specifier: string, fromDir: string, bun?: BunResolver | undefined): string | undefined;
@@ -0,0 +1,32 @@
1
+ import { existsSync, realpathSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ export function packageNameOf(specifier) {
4
+ const segments = specifier.split("/");
5
+ return specifier.startsWith("@") ? segments.slice(0, 2).join("/") : (segments[0] ?? specifier);
6
+ }
7
+ /** Walks node_modules upwards like Node does; returns the real path so transitive deps
8
+ * of an isolated-linker install are found as siblings. */
9
+ export function findInstalledPackageDir(packageName, fromDir) {
10
+ for (let dir = resolve(fromDir);; dir = resolve(dir, "..")) {
11
+ const packageDir = join(dir, "node_modules", packageName);
12
+ if (existsSync(join(packageDir, "package.json")))
13
+ return realpathSync(packageDir);
14
+ if (resolve(dir, "..") === dir)
15
+ return undefined;
16
+ }
17
+ }
18
+ const globalBun = typeof Bun !== "undefined" ? Bun : undefined;
19
+ /** Bun.resolveSync auto-installs from the registry (since 1.4.4) when no node_modules tree
20
+ * exists, so only call it for packages that are already installed. */
21
+ export function resolveInstalledSync(specifier, fromDir, bun = globalBun) {
22
+ if (bun === undefined)
23
+ return undefined;
24
+ if (findInstalledPackageDir(packageNameOf(specifier), fromDir) === undefined)
25
+ return undefined;
26
+ try {
27
+ return bun.resolveSync(specifier, fromDir);
28
+ }
29
+ catch {
30
+ return undefined;
31
+ }
32
+ }
@@ -1,14 +1,9 @@
1
- type BunResolver = {
2
- resolveSync: (id: string, from: string) => string;
3
- };
1
+ import { type BunResolver } from "./resolve-installed-package.ts";
4
2
  export type ResolveTailwindCliDeps = {
5
3
  readonly bun?: BunResolver;
6
4
  readonly cwd: string;
7
5
  };
8
6
  export declare function resolveTailwindCli(deps: ResolveTailwindCliDeps): string | undefined;
9
- /** Tailwind v4 resolves `@import "tailwindcss"` relative to the entry
10
- * CSS file. Skip renderer-web fallback (or fail build) when peer deps
11
- * aren't resolvable from that directory — e.g. Bun cache copies of
12
- * `@cosmicdrift/kumiko-renderer/web` outside a hoisted node_modules tree. */
7
+ /** Tailwind v4 resolves `@import "tailwindcss"` relative to the entry CSS file, so
8
+ * tailwindcss must be installed above it (not just next to a Bun cache copy of renderer-web). */
13
9
  export declare function canResolveTailwindStylesheet(entryCss: string, deps: ResolveTailwindCliDeps): boolean;
14
- export {};
@@ -1,34 +1,17 @@
1
1
  // Resolves the locally installed @tailwindcss/cli binary to its absolute path.
2
2
  // We avoid `bunx` because it still requests the registry manifest for a locally
3
3
  // installed package and fails with `FailedToOpenSocket` when offline.
4
- //
5
- // Kept separate from create-kumiko-server.ts so the Bun and resolution-failure
6
- // branches can be tested under Vitest/Node without booting the server.
7
- import { dirname, resolve } from "node:path";
4
+ import { dirname, join } from "node:path";
5
+ import { findInstalledPackageDir } from "./resolve-installed-package.js";
8
6
  export function resolveTailwindCli(deps) {
9
7
  if (deps.bun === undefined)
10
8
  return undefined;
11
- try {
12
- const pkgJsonPath = deps.bun.resolveSync("@tailwindcss/cli/package.json", deps.cwd);
13
- // bin: { tailwindcss: "./dist/index.mjs" } → absoluter Pfad
14
- return resolve(pkgJsonPath, "..", "dist", "index.mjs");
15
- }
16
- catch {
17
- return undefined;
18
- }
9
+ const cliDir = findInstalledPackageDir("@tailwindcss/cli", deps.cwd);
10
+ return cliDir === undefined ? undefined : join(cliDir, "dist", "index.mjs");
19
11
  }
20
- /** Tailwind v4 resolves `@import "tailwindcss"` relative to the entry
21
- * CSS file. Skip renderer-web fallback (or fail build) when peer deps
22
- * aren't resolvable from that directory — e.g. Bun cache copies of
23
- * `@cosmicdrift/kumiko-renderer/web` outside a hoisted node_modules tree. */
12
+ /** Tailwind v4 resolves `@import "tailwindcss"` relative to the entry CSS file, so
13
+ * tailwindcss must be installed above it (not just next to a Bun cache copy of renderer-web). */
24
14
  export function canResolveTailwindStylesheet(entryCss, deps) {
25
- if (deps.bun === undefined)
26
- return false;
27
- try {
28
- deps.bun.resolveSync("tailwindcss", dirname(entryCss));
29
- return true;
30
- }
31
- catch {
32
- return false;
33
- }
15
+ return (deps.bun !== undefined &&
16
+ findInstalledPackageDir("tailwindcss", dirname(entryCss)) !== undefined);
34
17
  }
@@ -16,6 +16,8 @@ export declare function addConfigAccessorFactory<T extends {
16
16
  export declare function loggerFromExtraContext(extraContext: Record<string, unknown>): Logger | undefined;
17
17
  /** Config key (`feature:config:name`) to value; wins over the key's env variable. */
18
18
  export type ConfigOverridesInput = Readonly<Record<string, string | number | boolean>>;
19
+ /** Env bridge first, validated app overrides on top: the one merge every boot function shares. */
20
+ export declare function buildConfigOverrides(registry: Registry, envSource: Record<string, string | undefined>, appOverrides: ConfigOverridesInput | undefined): import("@cosmicdrift/kumiko-bundled-features/config").AppConfigOverrides;
19
21
  type BootExtraContextOptions = {
20
22
  readonly db: DbConnection;
21
23
  readonly features: readonly FeatureDefinition[];
@@ -120,12 +120,16 @@ function bootCryptoContext(crypto) {
120
120
  ...(crypto.configCipher && { configEncryption: crypto.configCipher }),
121
121
  };
122
122
  }
123
- function createBootConfigResolver(registry, envSource, crypto, appOverrides) {
123
+ /** Env bridge first, validated app overrides on top: the one merge every boot function shares. */
124
+ export function buildConfigOverrides(registry, envSource, appOverrides) {
124
125
  const envOverrides = buildEnvConfigOverrides(registry, envSource);
126
+ return appOverrides
127
+ ? new Map([...envOverrides, ...validateAppOverrides(registry, appOverrides)])
128
+ : envOverrides;
129
+ }
130
+ function createBootConfigResolver(registry, envSource, crypto, appOverrides) {
125
131
  return createConfigResolver({
126
- appOverrides: appOverrides
127
- ? new Map([...envOverrides, ...validateAppOverrides(registry, appOverrides)])
128
- : envOverrides,
132
+ appOverrides: buildConfigOverrides(registry, envSource, appOverrides),
129
133
  ...(crypto.configCipher && { cipher: crypto.configCipher }),
130
134
  });
131
135
  }
@@ -136,7 +136,7 @@ export function composeProdFeatures(options, config, log) {
136
136
  // auth-email-password via composeFeatures, using the same feature list as
137
137
  // runDevApp and the per-app schema generator. Build it before any connection
138
138
  // so boot mode can validate wiring without opening Postgres or Redis sockets.
139
- const features = composeAppFeatures(options.features, bootOptionWiring.features, effectiveAuth);
139
+ const features = composeAppFeatures(options.features, bootOptionWiring.features, effectiveAuth, options.bundledLists);
140
140
  const bootCrypto = resolveBootCrypto(envSource, options.masterKey);
141
141
  validateComposedFeatures(features, {
142
142
  env: envSource,
@@ -2,6 +2,7 @@ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
2
2
  import type { Logger } from "@cosmicdrift/kumiko-framework/logging";
3
3
  import { createRedisCacheSyncBus } from "@cosmicdrift/kumiko-framework/redis";
4
4
  import { Redis } from "ioredis";
5
+ import { type DatabaseWaitClock } from "./boot/wait-for-database.ts";
5
6
  import type { RunProdAppOptions } from "./run-prod-app.ts";
6
7
  import type { ProdBootConfig, RegisterBootAbortCleanup } from "./run-prod-app-config.ts";
7
8
  export type ProdConnections = {
@@ -12,5 +13,5 @@ export type ProdConnections = {
12
13
  readonly redis: Redis;
13
14
  readonly cacheSync: ReturnType<typeof createRedisCacheSyncBus>;
14
15
  };
15
- export declare function connectProdDependencies(config: ProdBootConfig, log: Logger, registerAbortCleanup: RegisterBootAbortCleanup): Promise<ProdConnections>;
16
+ export declare function connectProdDependencies(config: ProdBootConfig, log: Logger, registerAbortCleanup: RegisterBootAbortCleanup, databaseWait?: DatabaseWaitClock): Promise<ProdConnections>;
16
17
  export declare function assertProdSchemaCurrent(db: DbConnection, migrations: RunProdAppOptions["migrations"], log: Logger): Promise<void>;
@@ -2,19 +2,19 @@ import { assertKumikoSchemaCurrent, SchemaDriftError, } from "@cosmicdrift/kumik
2
2
  import { createRedisCacheSyncBus, logRedisConnectionState, redisClientOptionsFromEnv, } from "@cosmicdrift/kumiko-framework/redis";
3
3
  import { Redis } from "ioredis";
4
4
  import { openDatabasePools, waitForKmsHealthy } from "./boot/shared-boot-steps.js";
5
- import { WAIT_FOR_DATABASE_DEADLINE_MS, waitForDatabase } from "./boot/wait-for-database.js";
5
+ import { WAIT_FOR_DATABASE_DEADLINE_MS, waitForDatabase, } from "./boot/wait-for-database.js";
6
6
  // Postgres + Redis. The Redis client is shared by idempotency, event-dedup,
7
7
  // entity-cache, rate-limit; failing to construct here surfaces the misconfig
8
8
  // immediately. `new Redis(...)` connects eagerly, so this must stay AFTER the
9
9
  // boot-mode exit.
10
- export async function connectProdDependencies(config, log, registerAbortCleanup) {
10
+ export async function connectProdDependencies(config, log, registerAbortCleanup, databaseWait) {
11
11
  const { bootKms, databaseUrl, redisUrl, envSource } = config;
12
12
  // KMS health gate: an app configured for crypto-shredding must not boot
13
13
  // against an unreachable key store — a silent start would answer every
14
14
  // PII read with 503. Runs BEFORE connections so an abort leaks nothing.
15
- const databaseWaitDeadline = Date.now() + WAIT_FOR_DATABASE_DEADLINE_MS;
15
+ const databaseWaitDeadline = (databaseWait?.now ?? Date.now)() + WAIT_FOR_DATABASE_DEADLINE_MS;
16
16
  if (bootKms.kms) {
17
- await waitForKmsHealthy(bootKms.kms, "runProdApp", databaseWaitDeadline);
17
+ await waitForKmsHealthy(bootKms.kms, "runProdApp", databaseWaitDeadline, databaseWait);
18
18
  }
19
19
  const pools = openDatabasePools(databaseUrl, envSource);
20
20
  const redis = new Redis(redisUrl, { maxRetriesPerRequest: null });
@@ -22,7 +22,11 @@ export async function connectProdDependencies(config, log, registerAbortCleanup)
22
22
  registerAbortCleanup(pools.closeOutsideTransactionDb);
23
23
  registerAbortCleanup(() => redis.disconnect());
24
24
  logRedisConnectionState(redis, log);
25
- await waitForDatabase(pools.db, { processName: "runProdApp", deadline: databaseWaitDeadline });
25
+ await waitForDatabase(pools.db, {
26
+ ...databaseWait,
27
+ processName: "runProdApp",
28
+ deadline: databaseWaitDeadline,
29
+ });
26
30
  // One bus per process: the tier resolver, the dispatcher and the file-provider
27
31
  // resolver all subscribe to it (invalidation across the API pods).
28
32
  const cacheSync = createRedisCacheSyncBus({
@@ -4,6 +4,7 @@ import { jobRunLoggerCallbacks } from "./boot/job-run-logger.js";
4
4
  import { startPiiEventBackfillOnBoot } from "./boot/pii-event-backfill-on-boot.js";
5
5
  import { recordAppStartedOnBoot } from "./boot/record-app-started-on-boot.js";
6
6
  import { makeDispatchSystemQuery, makeDispatchSystemWrite } from "./extra-routes-deps.js";
7
+ import { makeFilesForTenant } from "./files-for-tenant.js";
7
8
  import { buildProdAuthRoutes } from "./run-prod-app-auth.js";
8
9
  function collectProdExtraRoutes(options, config, composed) {
9
10
  const bootExtraRoutes = config.bootOptionWiring.extraRoutes;
@@ -126,6 +127,7 @@ export async function runProdWireHook(options, composed, connections, entrypoint
126
127
  db: connections.db,
127
128
  redis: connections.redis,
128
129
  registry: composed.registry,
130
+ filesForTenant: makeFilesForTenant(entrypoint.context),
129
131
  ...(bootCrypto.masterKeyProvider && { masterKeyProvider: bootCrypto.masterKeyProvider }),
130
132
  ...(bootCrypto.configCipher && { configEncryption: bootCrypto.configCipher }),
131
133
  metricsFor: createMetricsForFactory(entrypoint.observability.meter),
@@ -45,7 +45,8 @@ export function createProdAppHandle(input) {
45
45
  const { options, config, composed, connections, entrypoint, log } = input;
46
46
  const { fetchHandler, cspScriptHashes } = input.fetch;
47
47
  // Upgrade rejections (401/403/426...) are ordinary responses and get the same headers as fetchHandler.
48
- const securedUpgradeFetch = withSecurityHeaders(async (req, env) => stripNoRouteMatchHeader(await entrypoint.app.fetch(req, env)), options.securityHeaders, cspScriptHashes);
48
+ // Same read-only demo gate as fetchHandler: upgrade requests are GETs, but the socket then writes.
49
+ const securedUpgradeFetch = withSecurityHeaders(config.bootOptionWiring.wrapFetch(async (req, env) => stripNoRouteMatchHeader(await entrypoint.app.fetch(req, env))), options.securityHeaders, cspScriptHashes);
49
50
  const webSocketUpgradeFetch = async (req, env) => securedUpgradeFetch(req, env);
50
51
  let uninstallShutdownSignals;
51
52
  let uninstallFatalHandlers;
@@ -65,6 +65,8 @@ export async function runProdBootSeeds(run, entrypoint, dispatcherOptions) {
65
65
  admin: composed.effectiveAuth?.admin,
66
66
  shortcutSeeds: config.bootOptionWiring.shortcutSeeds,
67
67
  cipher: composed.bootCrypto.configCipher,
68
+ processName: "runProdApp",
69
+ ...(options.databaseWait && { databaseWait: options.databaseWait }),
68
70
  });
69
71
  const hasSeeds = config.bootOptionWiring.customSeeds.length > 0 || options.seedsDir !== undefined;
70
72
  const seedContext = hasSeeds ? buildSeedContext(run) : undefined;
@@ -1,9 +1,10 @@
1
+ import { existsSync } from "node:fs";
1
2
  import { join, resolve, sep } from "node:path";
2
3
  import { cachedResponse, computeStrongEtag, computeWeakEtag, createClientIpResolver, } from "@cosmicdrift/kumiko-framework/api";
3
4
  import { rankPreferredLocales } from "@cosmicdrift/kumiko-framework/i18n";
4
5
  import { resolveAndInjectPageHead } from "@cosmicdrift/kumiko-renderer/headless/apex";
5
6
  import { errorCodeOf } from "@cosmicdrift/kumiko-types/type-guards";
6
- import { ASSETS_DIR } from "./build-prod-bundle.js";
7
+ import { ASSETS_DIR, collectBuildScriptHashes } from "./build-prod-bundle.js";
7
8
  import { BUNDLED_ASSETS_DIST_DIR } from "./bundled-assets.js";
8
9
  import { CSP_HASHES_FILE, parseCspHashes } from "./csp-hashes.js";
9
10
  import { buildRequestBoundSystemQuery, } from "./request-bound-system-query.js";
@@ -67,7 +68,14 @@ function usesDefaultCsp(securityHeaders) {
67
68
  export async function resolveHostDispatchScriptHashes(staticDir, securityHeaders, defaultCspHashes) {
68
69
  return usesDefaultCsp(securityHeaders) ? defaultCspHashes : readBuildScriptHashes(staticDir);
69
70
  }
71
+ // Apps also write HTML into staticDir at boot (rendered landing pages), after kumiko build wrote
72
+ // the file; staticDir is server-owned, so hashing what is on disk never trusts request data.
70
73
  export async function readBuildScriptHashes(staticDir) {
74
+ const fromFile = await readBuildHashesFile(staticDir);
75
+ const onDisk = existsSync(staticDir) ? collectBuildScriptHashes(staticDir).scriptSrc : [];
76
+ return [...new Set([...fromFile, ...onDisk])].sort();
77
+ }
78
+ async function readBuildHashesFile(staticDir) {
71
79
  const file = await readStaticFile(join(staticDir, CSP_HASHES_FILE));
72
80
  const result = file ? parseCspHashes(new TextDecoder().decode(file.bytes)) : undefined;
73
81
  if (result?.kind === "found")
@@ -3,7 +3,7 @@ import type { AccountLockoutOptions, AccountUnlockOptions, AuthMailLocale, AuthP
3
3
  import type { SeedAdminOptions } from "@cosmicdrift/kumiko-bundled-features/auth-email-password/seeding";
4
4
  import type { ExtraRouteDefinition, KumikoServeEnv, PostAuthLandingResolver, SseBroker } from "@cosmicdrift/kumiko-framework/api";
5
5
  import type { KmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
6
- import type { FeatureDefinition, ValidateBootOptions } from "@cosmicdrift/kumiko-framework/engine";
6
+ import type { BundledListsOptions, FeatureDefinition, ValidateBootOptions } from "@cosmicdrift/kumiko-framework/engine";
7
7
  import type { AllInOneEntrypoint, ApiEntrypoint } from "@cosmicdrift/kumiko-framework/entrypoint";
8
8
  import type { ComposedEnvSchema, KumikoBootError } from "@cosmicdrift/kumiko-framework/env";
9
9
  import type { ErrorDocsConfig } from "@cosmicdrift/kumiko-framework/errors";
@@ -12,6 +12,7 @@ import type { ObservabilityProvider } from "@cosmicdrift/kumiko-framework/observ
12
12
  import type { MasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
13
13
  import type { PageHeadMeta, PageHeadResolver, PageHeadSystemQuery } from "@cosmicdrift/kumiko-renderer/headless/apex";
14
14
  import type { Redis } from "ioredis";
15
+ import type { DatabaseWaitClock } from "./boot/wait-for-database.ts";
15
16
  import type { BillingBootOption } from "./boot-options/billing-option.ts";
16
17
  import type { DemoModeOption } from "./boot-options/demo-mode.ts";
17
18
  import type { FileStorageBootOption } from "./boot-options/file-storage-option.ts";
@@ -29,7 +30,7 @@ export { parseRunMode } from "./boot/env-gate.ts";
29
30
  export { buildBunServeOptions } from "./bun-serve-options.ts";
30
31
  export { requireEnv } from "./require-env.ts";
31
32
  export { resolveUnsubscribeWiring, type UnsubscribeOption, } from "./resolve-unsubscribe-wiring.ts";
32
- export { addConfigAccessorFactory, buildBootExtraContext, type ConfigOverridesInput, loggerFromExtraContext, resolveAuthMail, } from "./run-prod-app-boot-context.ts";
33
+ export { addConfigAccessorFactory, buildBootExtraContext, buildConfigOverrides, type ConfigOverridesInput, loggerFromExtraContext, resolveAuthMail, } from "./run-prod-app-boot-context.ts";
33
34
  export { staticCachePolicy } from "./run-prod-app-static-files.ts";
34
35
  /** Wrapper API for the password-reset flow.
35
36
  *
@@ -263,6 +264,8 @@ export type RunProdAppOptions = {
263
264
  readonly features: readonly FeatureDefinition[];
264
265
  /** Opt-in boot-validator warnings — see ValidateBootOptions. */
265
266
  readonly validateBootOptions?: ValidateBootOptions;
267
+ /** One table block for every list of the bundled features, see ComposeFeaturesOptions.bundledLists. */
268
+ readonly bundledLists?: BundledListsOptions;
266
269
  /** Listen-Port. Default 3000 (or $PORT). */
267
270
  readonly port?: number;
268
271
  /** Ceiling Bun.serve buffers a request body to before any app-level
@@ -349,6 +352,9 @@ export type RunProdAppOptions = {
349
352
  * cannot start against an unreachable KMS. Without an adapter,
350
353
  * crypto-shredding remains disabled. */
351
354
  readonly kms?: KmsAdapter;
355
+ /** Replaces the real clock, sleep and log sink of the boot backoff for the database, KMS and
356
+ * seed waits; tests use it to run the retry path without real waiting. */
357
+ readonly databaseWait?: DatabaseWaitClock;
352
358
  /** 32-byte base64 key for blind-index HMACs on `lookupable` fields
353
359
  * (env: KUMIKO_BLIND_INDEX_KEY, `openssl rand -base64 32`). It is separate
354
360
  * from PLATFORM_KEK and not part of the KmsAdapter contract. It is required
@@ -54,7 +54,7 @@ export { parseRunMode } from "./boot/env-gate.js";
54
54
  export { buildBunServeOptions } from "./bun-serve-options.js";
55
55
  export { requireEnv } from "./require-env.js";
56
56
  export { resolveUnsubscribeWiring, } from "./resolve-unsubscribe-wiring.js";
57
- export { addConfigAccessorFactory, buildBootExtraContext, loggerFromExtraContext, resolveAuthMail, } from "./run-prod-app-boot-context.js";
57
+ export { addConfigAccessorFactory, buildBootExtraContext, buildConfigOverrides, loggerFromExtraContext, resolveAuthMail, } from "./run-prod-app-boot-context.js";
58
58
  export { staticCachePolicy } from "./run-prod-app-static-files.js";
59
59
  // Returned from runProdApp when KUMIKO_DRY_RUN_ENV is set AND envSource
60
60
  // was passed (= test-mode). The handle is intentionally inert — listen()
@@ -119,7 +119,7 @@ async function bootProdApp(options, registerAbortCleanup) {
119
119
  }
120
120
  return makeDryRunHandle();
121
121
  }
122
- const connections = await connectProdDependencies(config, log, registerAbortCleanup);
122
+ const connections = await connectProdDependencies(config, log, registerAbortCleanup, options.databaseWait);
123
123
  // Tier-composition auto-wire: if a feature carries a tenantTierResolver extension, the
124
124
  // resolver is built here, before the dispatcher exists. The app author only mounts
125
125
  // `createTierEngineFeature(opts)`; the framework does the wiring.
@@ -1,19 +1,21 @@
1
1
  import type { ServerOptions } from "@cosmicdrift/kumiko-framework/api";
2
2
  import type { KmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
3
3
  import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
4
- import type { FeatureDefinition, Registry, ValidateBootOptions } from "@cosmicdrift/kumiko-framework/engine";
4
+ import type { BundledListsOptions, FeatureDefinition, Registry, ValidateBootOptions } from "@cosmicdrift/kumiko-framework/engine";
5
5
  import type { WorkerEntrypoint } from "@cosmicdrift/kumiko-framework/entrypoint";
6
6
  import type { ComposedEnvSchema, KumikoBootError } from "@cosmicdrift/kumiko-framework/env";
7
7
  import type { JobRunnerOptions } from "@cosmicdrift/kumiko-framework/jobs";
8
8
  import { type ObservabilityProvider } from "@cosmicdrift/kumiko-framework/observability";
9
9
  import type { MasterKeyProvider, SecretsContext } from "@cosmicdrift/kumiko-framework/secrets";
10
10
  import type { Redis } from "ioredis";
11
+ import type { DatabaseWaitClock } from "./boot/wait-for-database.ts";
11
12
  import { type WorkerMetricsOptions, type WorkerMetricsServer } from "./boot/worker-metrics-server.ts";
12
13
  import type { BillingBootOption } from "./boot-options/billing-option.ts";
13
14
  import type { FileStorageBootOption } from "./boot-options/file-storage-option.ts";
14
15
  import type { KmsMode } from "./boot-options/kms-mode.ts";
15
16
  import type { MailBootOption } from "./boot-options/mail-option.ts";
16
17
  import { type SystemWireDeps } from "./extra-routes-deps.ts";
18
+ import type { ConfigOverridesInput } from "./run-prod-app-boot-context.ts";
17
19
  export type WorkerContextOption = Record<string, unknown> | ((deps: WorkerDeps) => Record<string, unknown>);
18
20
  export type WorkerDeps = {
19
21
  readonly db: DbConnection;
@@ -39,6 +41,8 @@ export type RunWorkerAppOptions = {
39
41
  readonly features: readonly FeatureDefinition[];
40
42
  /** Opt-in boot-validator warnings — see ValidateBootOptions. */
41
43
  readonly validateBootOptions?: ValidateBootOptions;
44
+ /** One table block for every list of the bundled features, see ComposeFeaturesOptions.bundledLists. Must match the API process. */
45
+ readonly bundledLists?: BundledListsOptions;
42
46
  /** Mount the auto-mixed config/user/tenant/auth-email-password features —
43
47
  * MUST match the API process's `includeBundled` value, otherwise API
44
48
  * and worker run with a diverging registry topology. Also controls
@@ -62,6 +66,12 @@ export type RunWorkerAppOptions = {
62
66
  /** Subject-key adapter for crypto-shredding — boot checks health()
63
67
  * before any connection (see RunProdAppOptions["kms"]). */
64
68
  readonly kms?: KmsAdapter;
69
+ /** App-level config values, same semantics and merge as RunProdAppOptions["appOverrides"];
70
+ * the worker renders the mail layouts and must resolve the same branding as the API. */
71
+ readonly appOverrides?: ConfigOverridesInput;
72
+ /** Replaces the real clock, sleep and log sink of the boot backoff for the database and
73
+ * KMS waits; tests use it to run the retry path without real waiting. */
74
+ readonly databaseWait?: DatabaseWaitClock;
65
75
  /** Blind-index key for lookupable fields (see
66
76
  * RunProdAppOptions["blindIndexKey"]). */
67
77
  readonly blindIndexKey?: string;
@@ -27,6 +27,7 @@ import { resolveBootOptionWiring } from "./boot-options/resolve-boot-options.js"
27
27
  import { assertStackBootRequirements } from "./boot-options/stack-boot-requirements.js";
28
28
  import { makeDispatchSystemQuery, makeDispatchSystemWrite, } from "./extra-routes-deps.js";
29
29
  import { installFatalErrorHandlers } from "./fatal-error-handlers.js";
30
+ import { makeFilesForTenant } from "./files-for-tenant.js";
30
31
  import { installProcessLogger, uninstallProcessLogger } from "./process-logger.js";
31
32
  function makeBootModeHandle() {
32
33
  return {
@@ -127,6 +128,7 @@ export async function runWorkerApp(options) {
127
128
  db,
128
129
  redis,
129
130
  registry,
131
+ filesForTenant: makeFilesForTenant(entrypoint.context),
130
132
  metricsFor: createMetricsForFactory(entrypoint.observability.meter),
131
133
  dispatchSystemWrite: makeDispatchSystemWrite(entrypoint.dispatcher),
132
134
  dispatchSystemQuery: makeDispatchSystemQuery(entrypoint.dispatcher),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-server-runtime",
3
- "version": "0.388.0",
3
+ "version": "0.389.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 tooling.",
5
5
  "license": "MIT",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -70,6 +70,10 @@
70
70
  "types": "./dist/renderer-web-fonts.d.ts",
71
71
  "default": "./dist/renderer-web-fonts.js"
72
72
  },
73
+ "./resolve-installed-package": {
74
+ "types": "./dist/resolve-installed-package.d.ts",
75
+ "default": "./dist/resolve-installed-package.js"
76
+ },
73
77
  "./resolve-tailwind-cli": {
74
78
  "types": "./dist/resolve-tailwind-cli.d.ts",
75
79
  "default": "./dist/resolve-tailwind-cli.js"
@@ -92,18 +96,21 @@
92
96
  }
93
97
  },
94
98
  "dependencies": {
95
- "@cosmicdrift/kumiko-bundled-features": "0.388.0",
96
- "@cosmicdrift/kumiko-framework": "0.388.0",
97
- "@cosmicdrift/kumiko-renderer": "0.388.0",
98
- "@cosmicdrift/kumiko-renderer-mail-html": "0.388.0",
99
- "@cosmicdrift/kumiko-types": "0.388.0",
100
- "bun-types": "^1.3.13",
99
+ "@cosmicdrift/kumiko-bundled-features": "0.389.0",
100
+ "@cosmicdrift/kumiko-framework": "0.389.0",
101
+ "@cosmicdrift/kumiko-renderer": "0.389.0",
102
+ "@cosmicdrift/kumiko-renderer-mail-html": "0.389.0",
103
+ "@cosmicdrift/kumiko-types": "0.389.0",
101
104
  "ioredis": "^5.10.1",
102
105
  "temporal-polyfill": "^0.3.2",
103
106
  "zod": "^4.4.3"
104
107
  },
108
+ "peerDependencies": {
109
+ "bun-types": "^1.4.4"
110
+ },
105
111
  "devDependencies": {
106
- "@cosmicdrift/kumiko-testing": "0.388.0"
112
+ "@cosmicdrift/kumiko-testing": "0.389.0",
113
+ "bun-types": "^1.4.4"
107
114
  },
108
115
  "publishConfig": {
109
116
  "registry": "https://registry.npmjs.org",
@@ -161,6 +168,10 @@
161
168
  "types": "./dist/renderer-web-fonts.d.ts",
162
169
  "default": "./dist/renderer-web-fonts.js"
163
170
  },
171
+ "./resolve-installed-package": {
172
+ "types": "./dist/resolve-installed-package.d.ts",
173
+ "default": "./dist/resolve-installed-package.js"
174
+ },
164
175
  "./resolve-tailwind-cli": {
165
176
  "types": "./dist/resolve-tailwind-cli.d.ts",
166
177
  "default": "./dist/resolve-tailwind-cli.js"
package/src/changes.json CHANGED
@@ -1,4 +1,65 @@
1
1
  [
2
+ {
3
+ "version": "0.389.0",
4
+ "type": "fix",
5
+ "title": "The renderer-web stylesheet fallback checks tailwindcss in node_modules",
6
+ "detail": "canResolveTailwindStylesheet looks for tailwindcss in the node_modules directories above the stylesheet, as Tailwind does, instead of asking Bun.resolveSync. On Bun 1.4.4 the resolver auto-installs tailwindcss next to a renderer copy in Bun's install cache, so a build without a node_modules tree took the fallback and then failed in Tailwind with \"Can't resolve 'tailwindcss'\". Such a build skips the fallback stylesheet again."
7
+ },
8
+ {
9
+ "version": "0.389.0",
10
+ "type": "improvement",
11
+ "title": "Package lookups no longer auto-install from the registry",
12
+ "detail": "The new @cosmicdrift/kumiko-server-runtime/resolve-installed-package export looks up node_modules before asking Bun.resolveSync, which installs missing packages from the registry since Bun 1.4.4. The prod build, the dev-server stylesheet lookup and the handbuch commands use it."
13
+ },
14
+ {
15
+ "version": "0.389.0",
16
+ "type": "improvement",
17
+ "title": "bundledLists sets one table block for every bundled list",
18
+ "detail": "composeFeatures takes bundledLists with a table block and an optional pinFirstColumn flag. The block is mixed into every entityList, projectionList and relatedList section of the bundled features, the foundation features user and tenant as well as the features an app mounts from @cosmicdrift/kumiko-bundled-features, before the boot validator runs. A key the screen sets itself wins, export is left out on relatedList sections, and pinFirstColumn pins the first visible column left where no column is pinned yet. grouping and totals are rejected in the shared block. App screens stay untouched. Features opt in with the new registrar method r.adoptsBundledLists(), which every bundled feature with lists calls and which feature packages such as Enterprise can call for their own lists; app features never do. No migration for apps. The option lives in AppConfig.bundledLists for the managed kumiko/schema.ts and in the run options of runProdApp, runDevApp and runWorkerApp, so kumiko check and the running app see the same screens."
19
+ },
20
+ {
21
+ "version": "0.389.0",
22
+ "type": "improvement",
23
+ "title": "Boot database backoff takes an injectable clock",
24
+ "detail": "The new databaseWait option on runProdApp and runWorkerApp swaps sleep, now and the log sink of the database, KMS and seed waits. A worker boot against an unreachable database is now tested to retry until the deadline, abort with the connection error and close its Redis clients."
25
+ },
26
+ {
27
+ "version": "0.389.0",
28
+ "type": "fix",
29
+ "title": "Default CSP covers inline scripts of HTML written after the build",
30
+ "detail": "At boot the hashes from csp-hashes.json are merged with the inline scripts found in the HTML files under staticDir, for the default CSP and for hostDispatch. Boot-rendered pages keep their inline scripts working without a custom securityHeaders.csp."
31
+ },
32
+ {
33
+ "version": "0.389.0",
34
+ "type": "fix",
35
+ "title": "Read-only demo mode blocks WebSocket upgrades",
36
+ "detail": "Upgrade requests count as mutating in the demo read-only gate and get 403 demo_read_only unless the route path is in allowedWritePaths. Socket routes in a read-only demo that need to stay open must be listed there."
37
+ },
38
+ {
39
+ "version": "0.389.0",
40
+ "type": "breaking",
41
+ "title": "BootOptionName drops demoMode, seeds and signals",
42
+ "detail": "No stack declared them in bootRequires, so their boot checks never ran. A name returns together with its check and a test once a stack needs it.",
43
+ "migration": "no known callers; remove \"demoMode\", \"seeds\" and \"signals\" from bootRequires options"
44
+ },
45
+ {
46
+ "version": "0.389.0",
47
+ "type": "improvement",
48
+ "title": "appOverrides work in runWorkerApp and runDevApp",
49
+ "detail": "Both boot functions take the same appOverrides option as runProdApp and share its merge, so worker-rendered mails and the dev server resolve the same tenant model, branding and SEO values as the API. The runSingleInstance:false warning names appOverrides next to mail, fileStorage and featureToggles."
50
+ },
51
+ {
52
+ "version": "0.389.0",
53
+ "type": "fix",
54
+ "title": "Boot seeds survive a short database outage",
55
+ "detail": "applyStandardBootSeeds re-runs the idempotent seed sequence with the database backoff on transient connection errors (ECONNREFUSED, connection class 08, too_many_connections) until the 90 second deadline. Non-transient errors still abort the boot at once."
56
+ },
57
+ {
58
+ "version": "0.389.0",
59
+ "type": "improvement",
60
+ "title": "filesForTenant in SystemWireDeps",
61
+ "detail": "The wire hook and the worker wireComponents receive filesForTenant, built from the AppContext after buildServer wired it. ApiEntrypoint and WorkerEntrypoint expose that context as entrypoint.context."
62
+ },
2
63
  {
3
64
  "version": "0.388.0",
4
65
  "type": "improvement",